🎮 Controller Connected
⚔️Q
💀X
🌀E
😤C
📢R
💨F
🛡️Z
💚T
🕹️
Right Stick
INITIALIZING OMNIVERSE ENGINE...
● Audio ● Renderer ● World ● Assets
TIP: Press F1 anytime to view keyboard shortcuts
MOMENTUM
ABILITY
💾 Saving...
4D TESSERACT
IMPOSSIBLE GEOMETRY WALK-THROUGH
XY
XY Plane Rotation
Rotation around the Z-axis. This is like spinning a top or turning a steering wheel.
3D Rotation
XZ
XZ Plane Rotation
Rotation around the Y-axis. Like a compass needle or spinning in place.
3D Rotation
XW
XW Plane Rotation
The X-axis rotates INTO the 4th dimension. Vertices "flip through" hyperspace - impossible in 3D!
✨ 4D Only
YZ
YZ Plane Rotation
Rotation around the X-axis. Like a wheel rolling forward or nodding your head.
3D Rotation
YW
YW Plane Rotation
The Y-axis (vertical) rotates into W. "Up" and "Ana" exchange places - hyper-vertical motion!
✨ 4D Only
ZW
ZW Plane Rotation
The Z-axis (depth) rotates into W. Forward becomes hyperforward - the deepest 4D rotation.
✨ 4D Only
The Outer Cube
You are in ordinary 3D space. Walk toward a wall to enter...
Press SPACE to enter portal
SHIFTING DIMENSIONS...
WASD Move Mouse Look SPACE Enter Portal ESC Exit
+W (ANA)
THE FOURTH DIRECTION
-W (KATA)
Watch the glowing vertices - they're not disappearing, they're rotating through you
💡
EUREKA
You understood something profound.
DIMENSIONAL THINKER
W-COORDINATE
+W (Ana) - Hyperward
W=0 - Our 3D slice
-W (Kata) - Anti-hyperward
⟳ HYPERCUBE INVERTING ⟳
FLATLAND
A ROMANCE OF MANY DIMENSIONS
THE PATTERN
How dimensions are built
·
0D POINT
1 vertex
1D LINE
2 vertices, 1 edge
2D SQUARE
4 vertices, 4 edges
3D CUBE
8 vertices, 12 edges
?
4D TESSERACT
? vertices, ? edges
Based on the pattern, how many vertices does a tesseract have?
THROUGH THE TESSERACT
Higher Dimensional Guide
🌀 What is 4D Space?

You exist in 3D space: you can move left/right, forward/backward, and up/down. But mathematically, there's no reason space has to stop at three dimensions.

The fourth spatial dimension (W) is a direction perpendicular to all three directions you know. It's impossible to point toward it in our universe, but mathematics lets us explore it.

What is a Tesseract?

A tesseract (or hypercube) is to a cube what a cube is to a square:

Dimensional Progression
0D: Point (no dimensions)
1D: Line (1 direction)
2D: Square (2 directions)
3D: Cube (3 directions)
4D: Tesseract (4 directions)

A tesseract has 16 vertices, 32 edges, 24 square faces, and 8 cubic cells. You're walking through one of those cells right now.

👁 Why Does It Look Strange?

What you see is a 3D shadow of a 4D object. Just as a 3D cube casts a 2D shadow on a wall, a 4D tesseract casts a 3D "shadow" that we can see.

The distortions you see are because the W-axis is being projected away - parts of the tesseract that extend into the 4th dimension appear squished or enlarged.

🔄 Understanding 4D Rotation

In 3D, we rotate around axes (X, Y, Z). In 4D, we rotate in planes. With 4 dimensions, there are 6 possible rotation planes.

Three are familiar (3D rotations). Three are impossible in our world (4D rotations).

🌐 3D Rotations (Familiar)
XY Plane
Rotation around Z-axis. Like spinning a top.
XZ Plane
Rotation around Y-axis. Like a compass needle.
YZ Plane
Rotation around X-axis. Like a wheel rolling.
4D Rotations (Impossible)

These rotations involve the W-axis. They're mathematically valid but physically impossible in our universe.

XW Plane
X rotates into W. Points "flip" through the 4th dimension.
YW Plane
Y rotates into W. Vertical becomes hypervertical.
ZW Plane
Z rotates into W. Depth becomes hyperdepth.
🏛 The 8 Cubic Cells

A tesseract contains 8 cubic cells - 3D "rooms" that share faces with each other in impossible ways. Each room you visit is one of these cells.

🚪 Why Rooms Feel Bigger Inside

In 4D, a room can have more interior volume than its 3D exterior suggests. The "extra" space extends into the W dimension.

Think of it like this: a flat shadow of a box looks like a square - you can't tell how deep it is. Similarly, a 3D "shadow" of a 4D room hides its W-depth.

🔮 Portals Between Cells

The portals connect cubic cells through their shared faces. When you step through a portal, you're moving in the W direction - even though it feels like you're walking forward.

This is why each room can lead to multiple others without overlapping. They're not stacked in 3D; they're arranged in 4D.

🧠 What You Look Like from 4D

A 4D being looking at you would see your entire 3D body at once - your skin, your organs, your skeleton - all simultaneously visible, like how you can see the entire 2D cross-section of an orange slice.

Your timeline would appear as a 4D "worm" - every moment of your existence laid out like a sculpture through time.

Time as a Fourth Dimension

Einstein showed that time is dimension-like. But the W-axis here is a fourth spatial dimension - not time. It's a direction you could theoretically walk in, if physics allowed it.

In this tesseract, you're exploring what it might feel like if that extra direction actually existed.

The Limits of Perception

Your brain evolved in 3D. It has no hardware for processing 4D space directly. What you're seeing is your visual cortex's best attempt at interpreting something it was never designed to understand.

The confusion, the wrongness, the sense that something is off - that's your 3D brain brushing against a higher reality it cannot fully grasp.

W-AXIS POSITION
-W (ANA) +W (KATA)
0.00
Fourth Dimensional Depth
STEP 1 OF 7
🌀
Welcome to 4D Space
You have crossed the event horizon and entered a tesseract - a four-dimensional hypercube. Everything you thought you knew about space is about to change.
📖 4D Glossary
Tesseract (Hypercube)
The 4D analog of a cube. It has 16 vertices, 32 edges, 24 square faces, and 8 cubic cells. Also called an 8-cell or octachoron.
W-Axis
The fourth spatial dimension, perpendicular to X, Y, and Z. Movement along W is impossible in our universe but mathematically valid.
Ana / Kata
Coined by Charles Hinton in 1888. "Ana" means movement in the +W direction, "Kata" means movement in the -W direction. Like up/down for the fourth dimension.
Stereographic Projection
A method of projecting higher-dimensional objects into lower dimensions. The tesseract you see is a 3D stereographic projection of a 4D object.
Rotation Plane
In 4D, rotation happens in a plane, not around an axis. Six rotation planes exist: XY, XZ, XW, YZ, YW, ZW. Three involve W and are impossible in 3D.
Cell (4D)
The 3D boundary element of a 4D shape. A tesseract has 8 cubic cells, just as a cube has 6 square faces.
Hyperplane
A 3D "slice" of 4D space. The room you're standing in is a hyperplane - a 3D cross-section of the tesseract.
Klein Bottle
A non-orientable surface that can only exist without self-intersection in 4D. It has no inside or outside - like a Möbius strip with no edges.
Flatland
An 1884 novella by Edwin Abbott exploring how 2D beings would perceive 3D. A useful analogy for understanding how 3D beings (us) perceive 4D.
4D Intuition
With practice, some mathematicians develop a "feel" for 4D space. Your brain can build new pathways for processing higher dimensions, though true 4D vision remains impossible.

⬤ EVENT HORIZON

You have approached the supermassive black hole at the galaxy's center. Beyond lies a gateway to the 4th dimension - a tesseract of impossible geometry where space folds upon itself.

Do you dare to enter?

1/60 Loading... Unknown
← → Arrow Keys or Tab to Navigate
🌌
ORBITAL PHYSICS
Real-time Keplerian mechanics
Gravitational Constant (G) 50,000
Controls the strength of gravity. Higher = faster orbits.
50,000
Black Hole Mass (M) 1,000
Mass of central body. Heavier = stronger pull.
1,000
Time Scale 0.50x
Speed of simulation. Slow down to observe, speed up to see patterns.
0.50x
Orbit Eccentricity 0.30
How elliptical orbits are. 0 = circular, 1 = parabolic.
0.30
Quick Scenarios:
ω = √(G·M / r³)
Kepler's Third Law: Angular velocity from gravity
Move mouse to show • Try the presets!
⚡ v6.32 MIND-BLOWING FEATURES
🎥 Planet Rider Cam
Ride along on a planet's orbital journey
🌀 Gravitational Lensing
Light bending around the supermassive black hole
💥 Planet Collisions
Planets can collide and create supernovae
⚙️ Auto-hide Panel
🔥 Universe Ignition
Destroyed & escaped planets are permanent. When all planets are gone, discover new galaxies to ignite. Share via QR code to let others visit your universe!
Approaching
APPROACHING
System-857
Desert World • Population: 78M
🏠 FAMILIAR GROUND - 66 previous visits
1,500
Distance (km)
12
Velocity (km/s)
STABLE
ETA (sec)
4/5 System-857 Desert
← → Arrow Keys or Tab to Navigate
🐺
Primal Ravager
Enemy Hero • Level 5
HP: 500/500
DMG: 25
ARM: 5
Level 5 • 1,250 / 2,000 XP
LEVIATHAN
GALAXY SIMULATION v10.32
0 Civilizations
0 Cycle
0:00 Playtime
🤖 PROBE INTEGRITY
100 / 100
ENERGY
100/100
Target
🐜 3D Ant Farm
Drag Orbit QE Rotate RF Tilt Scroll Zoom A Auto T Top 1-4 Views N Close
Mobs: 0
Trees: 0
Rocks: 0
Agents: 0
Fish Spots: 0
Zoom: 43%
🔥 IGNITE NEW UNIVERSE
You will be the FIRST OBSERVER to burn this reality into existence
60
Planets Awaiting Birth
2
Universe #
Possibilities
First Observer
Pioneer 0000
Your observation will collapse infinite potential into defined reality
Galaxy 1 has been fully explored. A new universe awaits your observation.
All skills, inventory, and progress will be preserved across realities.
🌌 GALAXY MANAGER
1
Galaxies
60
Total Planets
0
Visited
Galaxy #1
Scan to drop into this universe
🔥 Ignited by: Unknown
60 planets active
Visitors will see exact state: destroyed/escaped planets
Skills×
Mining 1
Woodcutting 1
Combat 1
Fishing 1
Cooking 1
Crafting 1
Crafting×
Shift+Click = Craft All
Advanced
⚔️ Legendary
🛡️ Equipment
🎒 (0/20)×
Right-click items to drop • Auto-drops lowest priority when full
⚔️ Gear×
⚔️ Empty
🛡️ Empty
💍 Empty
🔧 Empty
⚔️ +0
🛡️ +0
A
ACT
DODGE
⚔️
🌀
💚
🛡️
VICTORY
The enemy throne has fallen!
🤖
🧬
GENESIS ENGINE
EMERGENT CIVILIZATION SIMULATOR
0
Population
0
Factions
0
Age
0
Wars
🧬 GENESIS CONTROLS
Click anywhere to drop a seed particle
Divine Interventions
Click to plant seed
Speaking...
Listening...
Speak now...
Click to select
Copilot Companion
Hello, Explorer! I'm your Copilot Companion. I'll follow you on your journey and help with advice. What would you like to know?
Press V to voice chat • Space while open to speak • Esc to close
💬
0
World Chat 1 online
System
Welcome to World Chat! Press Enter to chat with other players.

Mind-Blowing Prompts

21 consensus prompts from 16 AI strategists
📜

THE ARCHIVIST SPEAKS

Memento Mori Protocol Active
Welcome back. I have been... waiting.
0
Total Deaths
0:00
Last Survival
0
This Session
🔧 Working...
Preparing...
🚀 Agent Fleet 0/10
Endpoint:
No agents deployed yet.
Click an agent type above to spawn.
📋 Agent Transcripts

AI Companion Settings

API Configuration

World Settings

Endpoint Profiles

Create different endpoint profiles to assign to your agent fleet. Each agent can use a different AI provider.

Default Agent Endpoint

Select which profile to use for new agents by default.

Quick Test

Voice Output

Enable Voice Response
Auto-speak Responses

Voice Input

Enable Voice Input
Continuous Conversation Mode

Companion Appearance

Show Particles
Enable Glow Effect

Behavior

RAPPID Settings

Import your RAPPID configuration file to automatically configure API endpoints and Azure TTS settings.

Connection Status

No endpoint configured

Reset

No Event
--:--
Cinematic Mode
'); * // Returns: <script>alert("xss")</script> */ const escapeHtml = (text) => { if (text === null || text === undefined) return ''; return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }; /** * Error recovery utilities for graceful failure handling * Provides retry logic, safe DOM queries, safe JSON parsing, and save data validation * @namespace ErrorRecovery */ const ErrorRecovery = { // Track error counts per operation errorCounts: new Map(), maxRetries: 3, retryDelay: 1000, /** * Wraps a function with automatic retry logic and exponential backoff * @param {function} fn - Async function to retry * @param {object} options - Retry options * @param {string} options.name - Operation name for logging (default: 'operation') * @param {number} options.retries - Number of retry attempts (default: 3) * @param {number} options.delay - Base delay between retries in ms (default: 1000) * @param {*} options.fallback - Fallback value or function if all retries fail * @returns {function} Wrapped function with retry logic * @example * const safeLoad = ErrorRecovery.withRetry(loadData, { * name: 'loadData', * retries: 3, * fallback: () => getDefaultData() * }); */ withRetry(fn, options = {}) { const { name = 'operation', retries = this.maxRetries, delay = this.retryDelay, fallback = null } = options; return async (...args) => { let lastError; for (let attempt = 0; attempt <= retries; attempt++) { try { return await fn(...args); } catch (err) { lastError = err; if (DEBUG_LOGGING) console.warn(`[ErrorRecovery] ${name} failed (attempt ${attempt + 1}/${retries + 1}):`, err.message); if (attempt < retries) { await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1))); } } } // All retries exhausted this.errorCounts.set(name, (this.errorCounts.get(name) || 0) + 1); if (fallback !== null) { if (DEBUG_LOGGING) console.log(`[ErrorRecovery] ${name} using fallback value`); return typeof fallback === 'function' ? fallback() : fallback; } throw lastError; }; }, // Safe DOM query with fallback safeQuerySelector(selector, fallback = null) { try { const el = document.querySelector(selector); return el || fallback; } catch (err) { if (DEBUG_LOGGING) console.warn('[ErrorRecovery] Invalid selector:', selector); return fallback; } }, // Safe JSON parse with fallback safeJSONParse(text, fallback = null) { try { return JSON.parse(text); } catch (err) { if (DEBUG_LOGGING) console.warn('[ErrorRecovery] JSON parse failed:', err.message); return fallback; } }, // v8.37: UNIVERSAL LOCALSTORAGE QUOTA HANDLER (8-Strategy Round 5 #1 - 8/8 UNANIMOUS) // Provides QuotaExceededError recovery with automatic cleanup safeLocalStorage: { // Track storage usage for intelligent cleanup storageUsage: new Map(), lastCleanup: 0, CLEANUP_COOLDOWN: 60000, // 1 minute between cleanups get(key, fallback = null) { try { const value = localStorage.getItem(key); return value !== null ? value : fallback; } catch (err) { if (DEBUG_LOGGING) console.warn('[ErrorRecovery] localStorage get failed:', err.message); return fallback; } }, /** * Sets a value in localStorage with automatic quota handling and cleanup * @param {string} key - Storage key * @param {string} value - Value to store * @param {object} options - Optional configuration * @param {boolean} options.critical - If true, prioritize this item during cleanup * @param {boolean} options.compress - Attempt to compress value before storing * @returns {boolean} True if successful, false otherwise */ set(key, value, options = {}) { const { critical = false, compress = false } = options; try { // Track size for LRU cleanup this.storageUsage.set(key, { size: value.length, timestamp: Date.now(), critical: critical }); localStorage.setItem(key, value); return true; } catch (err) { // Handle QuotaExceededError specifically if (err.name === 'QuotaExceededError' || err.code === 22 || err.code === 1014) { if (DEBUG_LOGGING) console.warn('[StorageQuota] Quota exceeded - attempting cleanup'); // Attempt automatic cleanup const freedSpace = this.performSmartCleanup(value.length, critical); if (freedSpace > 0) { try { localStorage.setItem(key, value); this.storageUsage.set(key, { size: value.length, timestamp: Date.now(), critical }); if (typeof showNotification === 'function') { showNotification(`Storage optimized - freed ${Math.round(freedSpace / 1024)}KB`, 'info'); } return true; } catch (retryErr) { if (DEBUG_LOGGING) console.error('[StorageQuota] Cleanup insufficient:', retryErr); } } // Show user-friendly error if (typeof showNotification === 'function') { showNotification('Storage full - please export saves and clear old data', 'warning'); } } else { if (DEBUG_LOGGING) console.warn('[ErrorRecovery] localStorage set failed:', err.message); } return false; } }, /** * Performs intelligent cleanup of localStorage to free space * @param {number} requiredSpace - Bytes needed * @param {boolean} protectCritical - Whether to avoid removing critical items * @returns {number} Bytes freed */ performSmartCleanup(requiredSpace, protectCritical = true) { // Avoid excessive cleanup attempts if (Date.now() - this.lastCleanup < this.CLEANUP_COOLDOWN) { return 0; } this.lastCleanup = Date.now(); let freedBytes = 0; const itemsToRemove = []; // Build list of removable items (LRU - Least Recently Used) const allKeys = []; for (let i = 0; i < localStorage.length; i++) { allKeys.push(localStorage.key(i)); } // Categorize by priority (non-critical, old backups, temp data) const candidates = allKeys .map(key => { const usage = this.storageUsage.get(key); const value = localStorage.getItem(key); const size = value ? value.length : 0; // Determine if removable const isBackup = key.includes('backup-') || key.includes('_backup'); const isTemp = key.includes('temp') || key.includes('cache'); const isCritical = usage?.critical || key.includes('leviathan-omniverse'); const age = Date.now() - (usage?.timestamp || 0); return { key, size, isBackup, isTemp, isCritical, age }; }) .filter(item => { // Remove temp data and old backups first if (item.isTemp) return true; if (item.isBackup && item.age > 86400000) return true; // 24h old backups if (!protectCritical && !item.isCritical) return true; return false; }) .sort((a, b) => b.age - a.age); // Oldest first // Remove items until we have enough space for (const item of candidates) { if (freedBytes >= requiredSpace * 1.2) break; // Free 20% extra try { localStorage.removeItem(item.key); this.storageUsage.delete(item.key); freedBytes += item.size; if (DEBUG_LOGGING) console.log(`[StorageQuota] Removed ${item.key} (${Math.round(item.size / 1024)}KB)`); } catch (e) { if (DEBUG_LOGGING) console.warn('[StorageQuota] Failed to remove:', item.key); } } return freedBytes; }, remove(key) { try { localStorage.removeItem(key); this.storageUsage.delete(key); return true; } catch (err) { return false; } }, /** * Get estimated storage usage * @returns {object} Storage statistics */ getStorageInfo() { let totalSize = 0; let itemCount = 0; for (let i = 0; i < localStorage.length; i++) { try { const key = localStorage.key(i); const value = localStorage.getItem(key); totalSize += (key.length + (value ? value.length : 0)) * 2; // UTF-16 chars = 2 bytes itemCount++; } catch (e) {} } return { totalSizeKB: Math.round(totalSize / 1024), totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2), itemCount: itemCount, estimatedQuotaMB: 5, // Most browsers provide 5-10MB usagePercent: Math.round((totalSize / (5 * 1024 * 1024)) * 100) }; } }, // Report error to user gracefully reportError(message, severity = 'error') { if (typeof showNotification === 'function') { showNotification(message, severity); } else { console.error('[ErrorRecovery]', message); } }, // v8.32: Validate and recover gameData structure validateGameData(data) { if (!data || typeof data !== 'object') { return this.createDefaultGameData(); } // Required fields with defaults const requiredFields = { resources: { wood: 0, ore: 0, fish: 0, gold: 0 }, inventory: [], skills: { mining: { level: 1, xp: 0 }, woodcutting: { level: 1, xp: 0 }, combat: { level: 1, xp: 0 }, fishing: { level: 1, xp: 0 }, cooking: { level: 1, xp: 0 }, crafting: { level: 1, xp: 0 } }, equipment: { weapon: null, armor: null, accessory: null, tool: null }, statistics: { treesChopped: 0, oresMined: 0, fishCaught: 0, mobsKilled: 0, itemsCrafted: 0 }, playtime: 0, visitedPlanets: [], achievements: {}, enchantments: {}, health: 100, maxHealth: 100, mana: 50, maxMana: 50, playerLevel: 1, playerXP: 0 }; let recoveredFields = 0; for (const [field, defaultValue] of Object.entries(requiredFields)) { if (data[field] === undefined || data[field] === null) { data[field] = JSON.parse(JSON.stringify(defaultValue)); recoveredFields++; } else if (typeof defaultValue === 'object' && !Array.isArray(defaultValue)) { // Check nested object fields for (const [subField, subDefault] of Object.entries(defaultValue)) { if (data[field][subField] === undefined) { data[field][subField] = subDefault; recoveredFields++; } } } } if (recoveredFields > 0) { debugLog('ErrorRecovery', `Recovered ${recoveredFields} missing fields in gameData`); } return data; }, // v8.32: Create fresh default game data createDefaultGameData() { debugLog('ErrorRecovery', 'Creating default gameData'); return { resources: { wood: 0, ore: 0, fish: 0, gold: 0 }, inventory: [], skills: { mining: { level: 1, xp: 0 }, woodcutting: { level: 1, xp: 0 }, combat: { level: 1, xp: 0 }, fishing: { level: 1, xp: 0 }, cooking: { level: 1, xp: 0 }, crafting: { level: 1, xp: 0 } }, equipment: { weapon: null, armor: null, accessory: null, tool: null }, statistics: { treesChopped: 0, oresMined: 0, fishCaught: 0, mobsKilled: 0, itemsCrafted: 0, poisDiscovered: 0 }, playtime: 0, visitedPlanets: [], achievements: {}, enchantments: {}, health: 100, maxHealth: 100, mana: 50, maxMana: 50, playerLevel: 1, playerXP: 0, lastSaved: Date.now() }; }, // v8.32: Attempt to recover corrupted save data attemptSaveRecovery(key = 'leviathan-omniverse') { try { // Try to get the raw data const raw = localStorage.getItem(key); if (!raw) { this.reportError('No save data found - starting fresh', 'info'); return this.createDefaultGameData(); } // Try to parse it let data; try { data = JSON.parse(raw); } catch (parseError) { // Try to recover partial JSON this.reportError('Save data corrupted - attempting recovery...', 'warning'); // Try to find valid JSON subset const fixed = this.attemptJSONRepair(raw); if (fixed) { data = fixed; this.reportError('Partial save data recovered!', 'info'); } else { this.reportError('Could not recover save - starting fresh', 'error'); return this.createDefaultGameData(); } } // Validate and fix structure return this.validateGameData(data); } catch (err) { debugLog('ErrorRecovery', 'Save recovery failed:', err); return this.createDefaultGameData(); } }, // v8.32: Attempt to repair malformed JSON attemptJSONRepair(raw) { try { // Common fixes for corrupted JSON let fixed = raw .replace(/,\s*}/g, '}') // Remove trailing commas .replace(/,\s*]/g, ']') // Remove trailing commas in arrays .replace(/\n/g, ' ') // Remove newlines .replace(/\t/g, ' '); // Remove tabs // Try parsing fixed version return JSON.parse(fixed); } catch (e) { // If still failing, try to extract just critical data try { // Look for resources object const resourceMatch = raw.match(/"resources"\s*:\s*{[^}]+}/); const skillsMatch = raw.match(/"skills"\s*:\s*{[^}]+}/); if (resourceMatch || skillsMatch) { const partial = this.createDefaultGameData(); if (resourceMatch) { try { partial.resources = JSON.parse('{' + resourceMatch[0].split(':').slice(1).join(':')); } catch (e) {} } return partial; } } catch (e2) {} return null; } } }; // ============================================ // v8.32: PROGRESS INDICATOR SYSTEM // Shows progress feedback for long-running operations // ============================================ const ProgressIndicator = { activeIndicators: new Map(), // Show a progress indicator with optional percentage show(id, message = 'Processing...', options = {}) { const { type = 'spinner', // 'spinner' | 'bar' | 'indeterminate' position = 'top-center', persistent = false } = options; // Remove existing indicator with same ID this.hide(id); const indicator = document.createElement('div'); indicator.id = `progress-${id}`; indicator.className = `progress-indicator progress-${type} progress-${position}`; indicator.setAttribute('role', 'progressbar'); indicator.setAttribute('aria-live', 'polite'); indicator.setAttribute('aria-label', message); if (type === 'bar') { indicator.innerHTML = `
${message}
0%
`; } else { indicator.innerHTML = `
${message}
`; } // Add styles if not present this.ensureStyles(); document.body.appendChild(indicator); this.activeIndicators.set(id, { element: indicator, persistent }); // Trigger entrance animation requestAnimationFrame(() => indicator.classList.add('visible')); return id; }, // Update progress (for bar type) update(id, percent, message = null) { const info = this.activeIndicators.get(id); if (!info) return; const fill = info.element.querySelector('.progress-bar-fill'); const percentEl = info.element.querySelector('.progress-percent'); const messageEl = info.element.querySelector('.progress-message'); if (fill) fill.style.width = `${Math.min(100, Math.max(0, percent))}%`; if (percentEl) percentEl.textContent = `${Math.round(percent)}%`; if (message && messageEl) messageEl.textContent = message; info.element.setAttribute('aria-valuenow', percent); }, // Hide and remove indicator hide(id, delay = 0) { const info = this.activeIndicators.get(id); if (!info) return; const remove = () => { info.element.classList.remove('visible'); setTimeout(() => { info.element.remove(); this.activeIndicators.delete(id); }, 300); }; if (delay > 0) { setTimeout(remove, delay); } else { remove(); } }, // Complete a progress bar with success animation complete(id, successMessage = 'Complete!') { this.update(id, 100, successMessage); const info = this.activeIndicators.get(id); if (info) { info.element.classList.add('success'); } this.hide(id, 1500); }, // Show error state error(id, errorMessage = 'Error occurred') { const info = this.activeIndicators.get(id); if (info) { const messageEl = info.element.querySelector('.progress-message'); if (messageEl) messageEl.textContent = errorMessage; info.element.classList.add('error'); } this.hide(id, 3000); }, // Ensure CSS styles are injected ensureStyles() { if (document.getElementById('progress-indicator-styles')) return; const style = document.createElement('style'); style.id = 'progress-indicator-styles'; style.textContent = ` .progress-indicator { position: fixed; z-index: 10000; background: rgba(0, 20, 40, 0.95); border: 1px solid rgba(0, 255, 255, 0.3); border-radius: 8px; padding: 12px 20px; backdrop-filter: blur(10px); opacity: 0; transform: translateY(-10px); transition: all 0.3s var(--ease-out-quart, cubic-bezier(0.25, 1, 0.5, 1)); pointer-events: none; } .progress-indicator.visible { opacity: 1; transform: translateY(0); } .progress-indicator.success { border-color: rgba(0, 255, 136, 0.5); } .progress-indicator.error { border-color: rgba(255, 68, 68, 0.5); } .progress-top-center { top: 80px; left: 50%; transform: translateX(-50%) translateY(-10px); } .progress-top-center.visible { transform: translateX(-50%) translateY(0); } .progress-content { display: flex; align-items: center; gap: 12px; color: #fff; font-size: 14px; } .progress-spinner { width: 20px; height: 20px; border: 2px solid rgba(0, 255, 255, 0.2); border-top-color: #0ff; border-radius: 50%; animation: progress-spin 0.8s linear infinite; } .progress-bar-container { width: 150px; height: 6px; background: rgba(255, 255, 255, 0.1); border-radius: 3px; overflow: hidden; } .progress-bar-fill { height: 100%; background: linear-gradient(90deg, #0ff, #00ff88); border-radius: 3px; transition: width 0.3s ease-out; } .progress-percent { font-size: 12px; color: #0ff; min-width: 35px; } @keyframes progress-spin { to { transform: rotate(360deg); } } `; document.head.appendChild(style); } }; window.ProgressIndicator = ProgressIndicator; // ============================================ // v7.0: PERFORMANCE UTILITIES // Debounce and throttle to prevent layout thrashing // ============================================ const UIPerformance = { // Debounce: delays execution until after wait ms of no calls debounce(fn, wait = 100) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => fn.apply(this, args), wait); }; }, // Throttle: executes at most once per wait ms throttle(fn, wait = 16) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= wait) { lastTime = now; fn.apply(this, args); } }; }, // RequestAnimationFrame wrapper for smooth UI updates rafUpdate(fn) { let rafId = null; return function(...args) { if (rafId) return; rafId = requestAnimationFrame(() => { fn.apply(this, args); rafId = null; }); }; }, // Batch DOM reads/writes to prevent layout thrashing batchUpdate(readFn, writeFn) { const data = readFn(); requestAnimationFrame(() => writeFn(data)); } }; // ============================================ // v8.27: ANIMATION EASING FUNCTIONS // Comprehensive easing library for smooth animations // Usage: Easing.easeOutCubic(t) where t is 0-1 // ============================================ const Easing = { // Linear (no easing) linear: t => t, // Quadratic easeInQuad: t => t * t, easeOutQuad: t => t * (2 - t), easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, // Cubic (smooth, commonly used) easeInCubic: t => t * t * t, easeOutCubic: t => 1 - Math.pow(1 - t, 3), easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2, // Quartic (more pronounced) easeInQuart: t => t * t * t * t, easeOutQuart: t => 1 - Math.pow(1 - t, 4), easeInOutQuart: t => t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2, // Exponential (dramatic) easeInExpo: t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)), easeOutExpo: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t), easeInOutExpo: t => { if (t === 0 || t === 1) return t; return t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2; }, // Elastic (spring-like bounce) easeOutElastic: t => { if (t === 0 || t === 1) return t; const c4 = (2 * Math.PI) / 3; return Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1; }, easeInElastic: t => { if (t === 0 || t === 1) return t; const c4 = (2 * Math.PI) / 3; return -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4); }, // Bounce (ball-drop effect) easeOutBounce: t => { const n1 = 7.5625, d1 = 2.75; if (t < 1 / d1) return n1 * t * t; if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75; if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375; return n1 * (t -= 2.625 / d1) * t + 0.984375; }, // Back (overshoot) easeOutBack: t => { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); }, easeInBack: t => { const c1 = 1.70158, c3 = c1 + 1; return c3 * t * t * t - c1 * t * t; }, // v8.30: Spring easing (natural physics-based motion) spring: (t, stiffness = 100, damping = 10) => { if (t === 0 || t === 1) return t; const mass = 1; const omega0 = Math.sqrt(stiffness / mass); const zeta = damping / (2 * Math.sqrt(stiffness * mass)); const omega1 = omega0 * Math.sqrt(1 - zeta * zeta); return 1 - Math.exp(-zeta * omega0 * t) * (Math.cos(omega1 * t) + (zeta * omega0 / omega1) * Math.sin(omega1 * t)); }, // v8.30: Smooth step (very smooth, natural feel) smoothStep: t => t * t * (3 - 2 * t), // v8.30: Smoother step (even smoother) smootherStep: t => t * t * t * (t * (t * 6 - 15) + 10), // v8.30: Fast-out-slow-in (anticipation effect) fastOutSlowIn: t => { if (t < 0.4) return 2.5 * t * t; if (t < 0.8) return -2.5 * t * t + 4 * t - 1.1; return 1; }, // Utility: Apply easing to animate between values animate(from, to, t, easingFn = 'easeOutCubic') { const ease = typeof easingFn === 'function' ? easingFn : (this[easingFn] || this.linear); return from + (to - from) * ease(Math.max(0, Math.min(1, t))); }, // v8.30: Create a custom cubic bezier easing function cubicBezier(x1, y1, x2, y2) { // Approximate cubic bezier for common use cases return t => { const cx = 3 * x1; const bx = 3 * (x2 - x1) - cx; const ax = 1 - cx - bx; const cy = 3 * y1; const by = 3 * (y2 - y1) - cy; const ay = 1 - cy - by; // Newton-Raphson iteration for x let x = t; for (let i = 0; i < 8; i++) { const xCalc = ((ax * x + bx) * x + cx) * x; const slope = (3 * ax * x + 2 * bx) * x + cx; if (Math.abs(slope) < 0.0001) break; x -= (xCalc - t) / slope; } return ((ay * x + by) * x + cy) * x; }; } }; // ============================================ // v7.31: TIMER REGISTRY SYSTEM (8-Strategy Cycle 10 Consensus) // Tracks all setInterval/setTimeout to prevent memory leaks // Addresses 10:1 ratio of setInterval vs clearInterval calls // ============================================ const TimerRegistry = { intervals: new Map(), // name -> { id, ms, fn } timeouts: new Map(), // name -> { id, ms } // Register and start an interval // v8.25: Added input validation and error handling setInterval(name, fn, ms) { // v8.25: Input validation if (!name || typeof fn !== 'function') return null; if (typeof ms !== 'number' || ms < 0) ms = 100; // Clear existing interval with same name if (this.intervals.has(name)) { clearInterval(this.intervals.get(name).id); } const id = setInterval(fn, ms); this.intervals.set(name, { id, ms, fn, created: Date.now() }); return id; }, // Register and start a timeout // v8.25: Added input validation and error handling setTimeout(name, fn, ms) { // v8.25: Input validation if (!name || typeof fn !== 'function') return null; if (typeof ms !== 'number' || ms < 0) ms = 0; // Clear existing timeout with same name if (this.timeouts.has(name)) { clearTimeout(this.timeouts.get(name).id); } const id = setTimeout(() => { this.timeouts.delete(name); try { fn(); // v8.25: Wrapped in try-catch } catch (e) { if (DEBUG_LOGGING) console.error('[TimerRegistry] Timeout callback error:', name, e); } }, ms); this.timeouts.set(name, { id, ms, created: Date.now() }); return id; }, // Clear a specific interval by name clearInterval(name) { if (this.intervals.has(name)) { clearInterval(this.intervals.get(name).id); this.intervals.delete(name); return true; } return false; }, // Clear a specific timeout by name clearTimeout(name) { if (this.timeouts.has(name)) { clearTimeout(this.timeouts.get(name).id); this.timeouts.delete(name); return true; } return false; }, // Clear a timer by name (tries both interval and timeout) clear(name) { const clearedInterval = this.clearInterval(name); const clearedTimeout = this.clearTimeout(name); return clearedInterval || clearedTimeout; }, // Pause all intervals (for tab visibility change) pauseAll() { this.intervals.forEach((data, name) => { clearInterval(data.id); data.paused = true; }); }, // Resume all paused intervals resumeAll() { this.intervals.forEach((data, name) => { if (data.paused) { data.id = setInterval(data.fn, data.ms); data.paused = false; } }); }, // Clear all timers (for cleanup) clearAll() { this.intervals.forEach((data) => clearInterval(data.id)); this.timeouts.forEach((data) => clearTimeout(data.id)); this.intervals.clear(); this.timeouts.clear(); }, // Get stats for debugging getStats() { return { activeIntervals: this.intervals.size, activeTimeouts: this.timeouts.size, intervalNames: Array.from(this.intervals.keys()), timeoutNames: Array.from(this.timeouts.keys()) }; } }; // Make globally accessible window.TimerRegistry = TimerRegistry; // ============================================ // v8.39: PAGE VISIBILITY MANAGER (8-Strategy Round 7 #1 - 8/8 UNANIMOUS) // Consolidates 7 duplicate visibilitychange event listeners into single handler // Prevents memory leaks and provides centralized pub/sub for visibility events // Fixes: Memory leak from duplicate listeners, inconsistent visibility handling // ============================================ const PageVisibilityManager = { subscribers: new Map(), // callback name -> callback function isVisible: !document.hidden, lastVisibilityChange: performance.now(), _initialized: false, /** * Initialize the manager and set up single event listener */ init() { if (this._initialized) return; this._initialized = true; // Single centralized listener - replaces 7 duplicate listeners document.addEventListener('visibilitychange', () => { const wasVisible = this.isVisible; this.isVisible = !document.hidden; this.lastVisibilityChange = performance.now(); Logger.debug('PageVisibility', `Tab ${this.isVisible ? 'visible' : 'hidden'}`); // Notify all subscribers this.subscribers.forEach((callback, name) => { try { callback(this.isVisible, wasVisible); } catch (err) { Logger.error('PageVisibility', `Subscriber "${name}" error:`, err); } }); }); Logger.info('PageVisibility', 'Manager initialized with centralized listener'); }, /** * Subscribe to visibility change events * @param {string} name - Unique identifier for this subscriber * @param {Function} callback - Function(isVisible, wasVisible) to call on visibility change */ subscribe(name, callback) { if (typeof callback !== 'function') { Logger.warn('PageVisibility', `Invalid callback for subscriber "${name}"`); return; } if (this.subscribers.has(name)) { Logger.warn('PageVisibility', `Subscriber "${name}" already exists, replacing`); } this.subscribers.set(name, callback); Logger.debug('PageVisibility', `Subscribed: ${name} (total: ${this.subscribers.size})`); }, /** * Unsubscribe from visibility change events * @param {string} name - Identifier of subscriber to remove */ unsubscribe(name) { const removed = this.subscribers.delete(name); if (removed) { Logger.debug('PageVisibility', `Unsubscribed: ${name}`); } return removed; }, /** * Get current visibility state * @returns {boolean} True if page is visible */ getVisibility() { return this.isVisible; }, /** * Get time since last visibility change (ms) * @returns {number} Milliseconds since last change */ getTimeSinceChange() { return performance.now() - this.lastVisibilityChange; }, /** * Get stats for debugging */ getStats() { return { isVisible: this.isVisible, subscriberCount: this.subscribers.size, subscribers: Array.from(this.subscribers.keys()), timeSinceChange: this.getTimeSinceChange() }; } }; // Initialize immediately PageVisibilityManager.init(); // Make globally accessible window.PageVisibilityManager = PageVisibilityManager; // ============================================ // v7.90: GLOBAL VECTOR3 POOL UTILITY // Shared Vector3 pool for reducing GC pressure across systems // Usage: GlobalVec3Pool.acquire() / release(vec) / temp(n) // ============================================ const GlobalVec3Pool = { pool: [], maxSize: 64, // Larger pool for cross-system sharing // Pre-allocated temp vectors for common calculations (no release needed) _temps: null, _tempIndex: 0, // Initialize temp vectors lazily _initTemps() { if (!this._temps) { this._temps = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ]; } }, // Get a temp vector (cycles through 8 pre-allocated vectors) // Use when you need a vector briefly within a single function temp() { this._initTemps(); const vec = this._temps[this._tempIndex]; this._tempIndex = (this._tempIndex + 1) & 7; // Fast modulo 8 return vec.set(0, 0, 0); }, // Get indexed temp vector (for functions needing multiple temps) // idx: 0-7, use specific indices for specific purposes tempAt(idx) { this._initTemps(); return this._temps[idx & 7].set(0, 0, 0); }, // Acquire a vector from pool (must release when done) // Use when vector needs to persist beyond immediate function acquire() { if (this.pool.length > 0) { return this.pool.pop().set(0, 0, 0); } return new THREE.Vector3(); }, // Return a vector to the pool release(vec) { if (vec && this.pool.length < this.maxSize) { this.pool.push(vec); } }, // Batch release multiple vectors releaseAll(...vecs) { for (const vec of vecs) { if (vec && this.pool.length < this.maxSize) { this.pool.push(vec); } } }, // Get pool stats for debugging getStats() { return { poolSize: this.pool.length, maxSize: this.maxSize, tempsInitialized: !!this._temps }; } }; // ============================================ // v8.35: AUDIO NODE POOL SYSTEM (8-Strategy Consensus Round 3 #1) // Reduces garbage collection pressure by reusing Web Audio nodes // 6/8 agent consensus: Performance, Code Quality, Educational, Mobile // Expected: 70-80% reduction in audio-related GC pauses // ============================================ const AudioNodePool = { oscillators: [], gainNodes: [], maxPoolSize: 32, // Reasonable limit to prevent memory bloat stats: { created: 0, reused: 0, released: 0 }, // Acquire an oscillator from pool or create new acquireOscillator(ctx) { if (!ctx) return null; if (this.oscillators.length > 0) { this.stats.reused++; return this.oscillators.pop(); } this.stats.created++; return ctx.createOscillator(); }, // Acquire a gain node from pool or create new acquireGainNode(ctx) { if (!ctx) return null; if (this.gainNodes.length > 0) { const node = this.gainNodes.pop(); // Reset gain to default node.gain.cancelScheduledValues(ctx.currentTime); node.gain.setValueAtTime(1.0, ctx.currentTime); this.stats.reused++; return node; } this.stats.created++; return ctx.createGain(); }, // Release oscillator back to pool (call after .stop()) releaseOscillator(osc) { if (!osc) return; // Disconnect to free up audio graph connections try { osc.disconnect(); } catch (e) { // Already disconnected, that's fine } // Only pool if under limit if (this.oscillators.length < this.maxPoolSize) { this.oscillators.push(osc); this.stats.released++; } }, // Release gain node back to pool releaseGainNode(gain) { if (!gain) return; try { gain.disconnect(); } catch (e) { // Already disconnected } if (this.gainNodes.length < this.maxPoolSize) { this.gainNodes.push(gain); this.stats.released++; } }, // Batch release nodes after complex sounds releaseAll(nodes) { for (const node of nodes) { if (node instanceof OscillatorNode) { this.releaseOscillator(node); } else if (node instanceof GainNode) { this.releaseGainNode(node); } } }, // Clear pool (for memory pressure situations) clear() { this.oscillators = []; this.gainNodes = []; }, // Get pool statistics getStats() { return { oscillatorsPooled: this.oscillators.length, gainNodesPooled: this.gainNodes.length, maxPoolSize: this.maxPoolSize, totalCreated: this.stats.created, totalReused: this.stats.reused, totalReleased: this.stats.released, reuseRate: this.stats.created > 0 ? ((this.stats.reused / (this.stats.created + this.stats.reused)) * 100).toFixed(1) + '%' : '0%' }; } }; // Make globally accessible window.GlobalVec3Pool = GlobalVec3Pool; // ============================================ // v8.31: DOM ELEMENT CACHE SYSTEM // Caches frequently accessed DOM elements to reduce getElementById calls // Improves performance in update loops and reduces layout thrashing // ============================================ const DOMCache = { _cache: new Map(), _hitCount: 0, _missCount: 0, // Get element by ID with caching get(id) { if (this._cache.has(id)) { this._hitCount++; return this._cache.get(id); } const el = document.getElementById(id); if (el) { this._cache.set(id, el); } this._missCount++; return el; }, // Pre-cache frequently used elements warmUp() { const frequentIds = [ // HUD elements 'health-text', 'health-progressbar', 'health-bar-fill', 'lumber-count', 'ore-count', 'fish-count', 'gold-count', // Skill bars 'bar-mining', 'bar-wood', 'bar-combat', 'bar-fishing', 'bar-cooking', 'bar-crafting', 'lvl-mining', 'lvl-wood', 'lvl-combat', 'lvl-fishing', 'lvl-cooking', 'lvl-crafting', // Panels 'skills-panel', 'crafting-panel', 'inventory-panel', 'equipment-panel', // Status displays 'fps-counter', 'debug-info', 'notification-container', // Ability cooldowns 'cooldown-q', 'cooldown-e', 'cooldown-r', 'cooldown-t', 'cooldown-f', 'cooldown-z', 'cooldown-x', 'cooldown-c', 'cooldown-b', 'cooldown-text-q', 'cooldown-text-e', 'cooldown-text-r', 'cooldown-text-t', 'cooldown-text-f', 'cooldown-text-z', 'cooldown-text-x', 'cooldown-text-c', // Companion 'companion-health-bar', 'companion-name', 'companion-status', // v8.33: Additional cached elements 'total-playtime', 'unified-playtime', 'talent-points-btn', // v8.33: Time UI elements 'time-indicator', 'time-icon', 'time-name', 'time-clock', 'time-effect', // v8.33: Touch ability bar elements 'touch-ability-bar', 'touch-ability-q', 'touch-ability-e', 'touch-ability-t', 'touch-ability-z', // v8.33: Screen reader announcements 'sr-announcements', 'health-progressbar' ]; frequentIds.forEach(id => this.get(id)); debugLog('DOMCache', `Warmed up ${this._cache.size} elements`); }, // Invalidate cache (for dynamic elements) invalidate(id) { if (id) { this._cache.delete(id); } else { this._cache.clear(); } }, // Get cache statistics getStats() { const total = this._hitCount + this._missCount; return { cacheSize: this._cache.size, hits: this._hitCount, misses: this._missCount, hitRate: total > 0 ? (this._hitCount / total * 100).toFixed(1) + '%' : 'N/A' }; } }; // Make globally accessible window.DOMCache = DOMCache; // Warm up cache on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => DOMCache.warmUp()); } else { // v8.31: Use requestIdleCallback for non-blocking cache warmup if (typeof requestIdleCallback !== 'undefined') { requestIdleCallback(() => DOMCache.warmUp(), { timeout: 1000 }); } else { setTimeout(() => DOMCache.warmUp(), 100); } } // ============================================ // v8.27: RESOURCE MANAGER // Centralized Three.js resource cleanup and tracking // Prevents memory leaks from unreleased geometries/materials/textures // ============================================ const ResourceManager = { // Track registered resources resources: { geometries: new Set(), materials: new Set(), textures: new Set(), meshes: new Set() }, // Register a resource for tracking track(resource, type = 'auto') { if (!resource) return resource; // Auto-detect type if (type === 'auto') { if (resource.isGeometry || resource.isBufferGeometry) type = 'geometries'; else if (resource.isMaterial) type = 'materials'; else if (resource.isTexture) type = 'textures'; else if (resource.isMesh) type = 'meshes'; else return resource; // Unknown type, don't track } if (this.resources[type]) { this.resources[type].add(resource); } return resource; }, // Dispose a single resource dispose(resource) { if (!resource) return; // Remove from tracking for (const type of Object.keys(this.resources)) { this.resources[type].delete(resource); } // Dispose based on type if (resource.isMesh) { if (resource.geometry) this.dispose(resource.geometry); if (resource.material) { if (Array.isArray(resource.material)) { resource.material.forEach(m => this.dispose(m)); } else { this.dispose(resource.material); } } } else if (resource.isMaterial) { // Dispose any maps on the material const mapProps = ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'aoMap', 'emissiveMap', 'alphaMap', 'envMap']; mapProps.forEach(prop => { if (resource[prop]) { resource[prop].dispose(); } }); resource.dispose(); } else if (resource.dispose) { resource.dispose(); } }, // Dispose all tracked resources (for scene cleanup) disposeAll() { let disposed = 0; // Dispose in order: meshes, then materials, then textures, then geometries ['meshes', 'materials', 'textures', 'geometries'].forEach(type => { this.resources[type].forEach(resource => { try { if (resource.dispose) resource.dispose(); disposed++; } catch (e) { if (DEBUG_LOGGING) console.warn('[ResourceManager] Dispose error:', e.message); } }); this.resources[type].clear(); }); if (DEBUG_LOGGING) console.log(`[ResourceManager] Disposed ${disposed} resources`); return disposed; }, // Get stats for debugging getStats() { return { geometries: this.resources.geometries.size, materials: this.resources.materials.size, textures: this.resources.textures.size, meshes: this.resources.meshes.size, total: Object.values(this.resources).reduce((sum, set) => sum + set.size, 0) }; }, // Dispose a Three.js object hierarchy recursively disposeObject3D(object) { if (!object) return; // Traverse and dispose all children object.traverse(child => { if (child.geometry) { child.geometry.dispose(); } if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(m => { if (m.map) m.map.dispose(); m.dispose(); }); } else { if (child.material.map) child.material.map.dispose(); child.material.dispose(); } } }); // Remove from parent if (object.parent) { object.parent.remove(object); } } }; // Make globally accessible window.ResourceManager = ResourceManager; // ============================================ // v8.30: PERFORMANCE MONITOR // FPS tracking, memory warnings, and performance metrics // ============================================ const PerformanceMonitor = { // FPS tracking fps: 60, frameCount: 0, lastFPSUpdate: 0, fpsHistory: [], maxHistoryLength: 60, // Memory tracking lastMemoryCheck: 0, memoryCheckInterval: 5000, // Check every 5 seconds memoryWarningThreshold: 200 * 1024 * 1024, // 200MB memoryWarningShown: false, // Performance thresholds lowFPSThreshold: 25, criticalFPSThreshold: 15, lowFPSWarningCooldown: 30000, // 30s between warnings lastLowFPSWarning: 0, // UI element reference fpsDisplay: null, // Initialize the monitor init() { this.lastFPSUpdate = performance.now(); this.createFPSDisplay(); debugLog('PerformanceMonitor', 'Initialized'); }, // Create FPS display element createFPSDisplay() { if (this.fpsDisplay) return; this.fpsDisplay = document.createElement('div'); this.fpsDisplay.id = 'fps-monitor'; this.fpsDisplay.setAttribute('aria-label', 'Performance monitor'); this.fpsDisplay.setAttribute('role', 'status'); this.fpsDisplay.setAttribute('aria-live', 'polite'); this.fpsDisplay.style.cssText = ` position: fixed; top: 5px; left: 5px; background: rgba(0, 0, 0, 0.7); color: #0f0; padding: 4px 8px; font-family: monospace; font-size: 11px; border-radius: 4px; z-index: var(--z-ui-front, 200); pointer-events: none; opacity: 0.8; display: none; min-width: 60px; `; this.fpsDisplay.textContent = 'FPS: --'; document.body.appendChild(this.fpsDisplay); }, // Toggle FPS display visibility toggle() { if (this.fpsDisplay) { const isVisible = this.fpsDisplay.style.display !== 'none'; this.fpsDisplay.style.display = isVisible ? 'none' : 'block'; return !isVisible; } return false; }, // Show FPS display show() { if (this.fpsDisplay) { this.fpsDisplay.style.display = 'block'; } }, // Hide FPS display hide() { if (this.fpsDisplay) { this.fpsDisplay.style.display = 'none'; } }, // Update frame count (call each frame) tick() { this.frameCount++; const now = performance.now(); // Update FPS every second if (now - this.lastFPSUpdate >= 1000) { this.fps = Math.round((this.frameCount * 1000) / (now - this.lastFPSUpdate)); this.frameCount = 0; this.lastFPSUpdate = now; // Store in history this.fpsHistory.push(this.fps); if (this.fpsHistory.length > this.maxHistoryLength) { this.fpsHistory.shift(); } // Update display this.updateDisplay(); // Check for low FPS warning this.checkFPSWarning(now); } // Periodic memory check if (now - this.lastMemoryCheck >= this.memoryCheckInterval) { this.checkMemory(now); } }, // Update the FPS display updateDisplay() { if (!this.fpsDisplay || this.fpsDisplay.style.display === 'none') return; // Color code by performance let color = '#0f0'; // Green - good if (this.fps < this.lowFPSThreshold) { color = '#ff0'; // Yellow - warning } if (this.fps < this.criticalFPSThreshold) { color = '#f00'; // Red - critical } this.fpsDisplay.style.color = color; this.fpsDisplay.textContent = `FPS: ${this.fps}`; // Add memory info if available if (performance.memory) { const usedMB = Math.round(performance.memory.usedJSHeapSize / (1024 * 1024)); this.fpsDisplay.textContent += ` | ${usedMB}MB`; } }, // Check for low FPS and warn user checkFPSWarning(now) { if (this.fps < this.lowFPSThreshold && now - this.lastLowFPSWarning > this.lowFPSWarningCooldown) { this.lastLowFPSWarning = now; // Calculate average FPS const avgFPS = this.getAverageFPS(); if (avgFPS < this.lowFPSThreshold) { const message = this.fps < this.criticalFPSThreshold ? `Performance critical: ${this.fps} FPS. Consider closing other tabs.` : `Low performance detected: ${this.fps} FPS`; if (typeof showNotification === 'function') { showNotification(message, 'warning'); } debugWarn('PerformanceMonitor', message); } } }, // Check memory usage checkMemory(now) { this.lastMemoryCheck = now; // Chrome-only API if (!performance.memory) return; const usedHeap = performance.memory.usedJSHeapSize; const totalHeap = performance.memory.totalJSHeapSize; const heapLimit = performance.memory.jsHeapSizeLimit; // Warn if using more than threshold if (usedHeap > this.memoryWarningThreshold && !this.memoryWarningShown) { this.memoryWarningShown = true; const usedMB = Math.round(usedHeap / (1024 * 1024)); const limitMB = Math.round(heapLimit / (1024 * 1024)); const message = `High memory usage: ${usedMB}MB. Consider saving and refreshing.`; if (typeof showNotification === 'function') { showNotification(message, 'warning'); } debugWarn('PerformanceMonitor', message, { usedMB, limitMB }); } // Reset warning if memory drops if (usedHeap < this.memoryWarningThreshold * 0.7) { this.memoryWarningShown = false; } }, // Get average FPS over history getAverageFPS() { if (this.fpsHistory.length === 0) return 60; return Math.round(this.fpsHistory.reduce((a, b) => a + b, 0) / this.fpsHistory.length); }, // Get performance stats getStats() { const stats = { currentFPS: this.fps, averageFPS: this.getAverageFPS(), minFPS: this.fpsHistory.length > 0 ? Math.min(...this.fpsHistory) : 60, maxFPS: this.fpsHistory.length > 0 ? Math.max(...this.fpsHistory) : 60 }; if (performance.memory) { stats.memoryUsedMB = Math.round(performance.memory.usedJSHeapSize / (1024 * 1024)); stats.memoryTotalMB = Math.round(performance.memory.totalJSHeapSize / (1024 * 1024)); stats.memoryLimitMB = Math.round(performance.memory.jsHeapSizeLimit / (1024 * 1024)); } if (typeof ResourceManager !== 'undefined') { stats.resources = ResourceManager.getStats(); } return stats; } }; // Make globally accessible window.PerformanceMonitor = PerformanceMonitor; // ============================================ // v7.91: FLOATER POSITION HELPER // Avoids .clone().add(new THREE.Vector3(...)) allocations in spawnFloater calls // ============================================ function getFloaterPos(basePos, offsetY, offsetX = 0, offsetZ = 0) { const pos = GlobalVec3Pool.temp(); pos.set( basePos.x + offsetX, basePos.y + offsetY, basePos.z + offsetZ ); return pos; } // ============================================ // v7.79: FOCUS TRAP UTILITY (Accessibility Enhancement) // Reusable focus trapping for modal dialogs // ============================================ const FocusTrap = { // Focusable element selector FOCUSABLE_SELECTOR: 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', // Active focus traps activeTraps: new Map(), // Create and activate a focus trap for a modal element create(modalElement, options = {}) { if (!modalElement) return null; const focusableElements = modalElement.querySelectorAll(this.FOCUSABLE_SELECTOR); const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; // Store previous focus to restore later const previousFocus = document.activeElement; // Focus first element unless overridden if (options.initialFocus) { options.initialFocus.focus(); } else if (firstFocusable) { firstFocusable.focus(); } // Tab trapping handler const trapHandler = (e) => { if (e.key !== 'Tab') return; // Refresh focusable elements in case DOM changed const currentFocusable = modalElement.querySelectorAll(this.FOCUSABLE_SELECTOR); const first = currentFocusable[0]; const last = currentFocusable[currentFocusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last?.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first?.focus(); } } }; // Escape key handler (optional close callback) const escapeHandler = (e) => { if (e.key === 'Escape' && options.onEscape) { options.onEscape(); } }; modalElement.addEventListener('keydown', trapHandler); modalElement.addEventListener('keydown', escapeHandler); const trapId = 'trap-' + Date.now(); const trapData = { modalElement, trapHandler, escapeHandler, previousFocus }; this.activeTraps.set(trapId, trapData); return trapId; }, // Remove focus trap and restore previous focus destroy(trapId) { if (!this.activeTraps.has(trapId)) return false; const trap = this.activeTraps.get(trapId); trap.modalElement.removeEventListener('keydown', trap.trapHandler); trap.modalElement.removeEventListener('keydown', trap.escapeHandler); // Restore previous focus if (trap.previousFocus && trap.previousFocus.focus) { trap.previousFocus.focus(); } this.activeTraps.delete(trapId); return true; }, // Destroy all active traps destroyAll() { this.activeTraps.forEach((trap, id) => this.destroy(id)); } }; // Make globally accessible window.FocusTrap = FocusTrap; // ============================================ // v8.32: KEYBOARD NAVIGATION MANAGER // Enhanced keyboard navigation for game UI // ============================================ const KeyboardNav = { // Track current focus zone currentZone: 'main', // Define navigation zones with their focusable selectors zones: { main: { selector: '#rts-toggle-buttons .rts-toggle-btn', loop: true }, skills: { selector: '#skills-panel button, #skills-panel [tabindex="0"]', parent: '#skills-panel', loop: true }, crafting: { selector: '#crafting-panel button, #crafting-panel .craft-btn', parent: '#crafting-panel', loop: true }, inventory: { selector: '#inventory-panel .inv-slot, #inventory-panel button', parent: '#inventory-panel', loop: true }, equipment: { selector: '#equipment-panel .equip-slot, #equipment-panel button', parent: '#equipment-panel', loop: true } }, // Initialize keyboard navigation init() { document.addEventListener('keydown', (e) => this.handleKeydown(e)); debugLog('KeyboardNav', 'Keyboard navigation initialized'); }, // Handle arrow key navigation within zones handleKeydown(e) { // Don't interfere with input fields const tag = document.activeElement.tagName.toLowerCase(); if (tag === 'input' || tag === 'textarea' || tag === 'select') return; // F1 key shows keyboard help if (e.key === 'F1') { e.preventDefault(); this.showKeyboardHelp(); return; } // Arrow key navigation within current zone if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { const zone = this.getCurrentZone(); if (zone) { this.navigateInZone(zone, e.key, e); } } // Tab key respects zones when panel is open if (e.key === 'Tab') { this.handleTabNavigation(e); } }, // Detect current focus zone based on active element getCurrentZone() { const active = document.activeElement; if (!active) return null; for (const [name, zone] of Object.entries(this.zones)) { if (zone.parent) { const parent = document.querySelector(zone.parent); if (parent && parent.contains(active)) { return { name, ...zone }; } } } // Check if in main zone const mainBtns = document.querySelectorAll(this.zones.main.selector); for (const btn of mainBtns) { if (btn === active) { return { name: 'main', ...this.zones.main }; } } return null; }, // Navigate within a zone using arrow keys navigateInZone(zone, key, event) { const elements = document.querySelectorAll(zone.selector); if (elements.length === 0) return; const active = document.activeElement; let currentIndex = Array.from(elements).indexOf(active); if (currentIndex === -1) { elements[0]?.focus(); event.preventDefault(); return; } let nextIndex = currentIndex; if (key === 'ArrowDown' || key === 'ArrowRight') { nextIndex = zone.loop ? (currentIndex + 1) % elements.length : Math.min(currentIndex + 1, elements.length - 1); } else if (key === 'ArrowUp' || key === 'ArrowLeft') { nextIndex = zone.loop ? (currentIndex - 1 + elements.length) % elements.length : Math.max(currentIndex - 1, 0); } if (nextIndex !== currentIndex) { elements[nextIndex]?.focus(); event.preventDefault(); } }, // Smart tab navigation that respects panel state handleTabNavigation(e) { // Let tab work normally in open panels for (const panelName of ['skills', 'crafting', 'inventory', 'equipment']) { if (rtsPanelState[panelName]) { // Focus is managed by panel's focus trap or natural tab order return; } } }, // v8.38: Enhanced Interactive Keyboard Shortcuts Reference (8-Strategy Round 6 #1 - 6/8 votes) // Features: Search, categories, context awareness, ARIA support showKeyboardHelp() { // Remove existing help modal const existing = document.getElementById('keyboard-help-modal'); if (existing) { existing.remove(); return; } // Comprehensive shortcut database const shortcuts = [ // Movement { key: 'W/A/S/D', action: 'Move character', category: 'Movement', context: 'always', searchTerms: 'walk run move wasd' }, { key: 'Space', action: 'Jump / Interact with portals', category: 'Movement', context: 'always', searchTerms: 'jump leap portal enter' }, { key: 'Shift', action: 'Sprint (hold)', category: 'Movement', context: 'always', searchTerms: 'run fast sprint speed' }, { key: 'F', action: 'Dash forward quickly', category: 'Combat', context: 'always', searchTerms: 'dash dodge evade quick' }, // Combat { key: 'Q', action: 'Power Strike ability', category: 'Combat', context: 'always', searchTerms: 'attack ability skill power strike' }, { key: 'E', action: 'Whirlwind ability', category: 'Combat', context: 'always', searchTerms: 'attack ability skill whirlwind aoe' }, { key: 'R', action: 'War Cry ability', category: 'Combat', context: 'always', searchTerms: 'buff ability skill war cry boost' }, { key: 'T', action: 'Heal ability', category: 'Combat', context: 'always', searchTerms: 'heal restore health ability' }, { key: '1-5', action: 'Quick-cast abilities', category: 'Combat', context: 'always', searchTerms: 'ability quick cast hotkey' }, // Panels & UI { key: 'K', action: 'Open Skills panel', category: 'Panels', context: 'always', searchTerms: 'skills level up stats' }, { key: 'P', action: 'Open Crafting panel', category: 'Panels', context: 'always', searchTerms: 'craft create make items' }, { key: 'I', action: 'Open Inventory', category: 'Panels', context: 'always', searchTerms: 'inventory items bag storage' }, { key: 'G', action: 'Open Equipment panel', category: 'Panels', context: 'always', searchTerms: 'gear equipment armor weapons' }, { key: 'M', action: 'Open Galaxy Manager', category: 'Panels', context: 'always', searchTerms: 'galaxy map universe travel' }, { key: 'H', action: 'Toggle Nexus Hub', category: 'Panels', context: 'always', searchTerms: 'nexus hub menu command center' }, { key: 'C', action: 'Toggle Cinematic Mode', category: 'Panels', context: 'always', searchTerms: 'cinematic view camera screenshot' }, // Audio { key: 'U', action: 'Master Audio Mute toggle', category: 'Audio', context: 'always', searchTerms: 'mute sound audio volume quiet' }, // Navigation { key: 'Tab', action: 'Cycle focus through UI elements', category: 'Navigation', context: 'always', searchTerms: 'tab focus cycle navigate' }, { key: 'Arrows', action: 'Navigate menus and selections', category: 'Navigation', context: 'menus', searchTerms: 'arrow keys navigate menu' }, { key: 'Enter', action: 'Activate focused button', category: 'Navigation', context: 'always', searchTerms: 'enter select activate confirm' }, { key: 'Escape', action: 'Close panel/menu/modal', category: 'Navigation', context: 'always', searchTerms: 'escape close exit cancel' }, // Help & System { key: 'F1 / ?', action: 'Show this help menu', category: 'Help', context: 'always', searchTerms: 'help shortcuts keyboard reference' }, { key: 'F11', action: 'Toggle fullscreen', category: 'System', context: 'always', searchTerms: 'fullscreen full screen maximize' } ]; const modal = document.createElement('div'); modal.id = 'keyboard-help-modal'; modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-labelledby', 'kb-help-title'); modal.innerHTML = `

Keyboard Shortcuts

`; // Add styles if not present this.ensureHelpStyles(); document.body.appendChild(modal); // Initialize interactive features const searchInput = modal.querySelector('#kb-search'); const shortcutsList = modal.querySelector('#kb-shortcuts-list'); const resultsCount = modal.querySelector('#kb-results-count'); const filterButtons = modal.querySelectorAll('.kb-filter-btn'); let currentFilter = 'all'; let currentSearch = ''; // Render shortcuts function const renderShortcuts = () => { const filtered = shortcuts.filter(s => { const matchesCategory = currentFilter === 'all' || s.category === currentFilter; const matchesSearch = currentSearch === '' || s.key.toLowerCase().includes(currentSearch) || s.action.toLowerCase().includes(currentSearch) || s.searchTerms.toLowerCase().includes(currentSearch); return matchesCategory && matchesSearch; }); // Group by category const grouped = {}; filtered.forEach(s => { if (!grouped[s.category]) grouped[s.category] = []; grouped[s.category].push(s); }); // Render shortcutsList.innerHTML = Object.entries(grouped).map(([category, items]) => `

${category}

${items.map(item => `
${item.key} ${item.action}
`).join('')}
`).join(''); // Update results count resultsCount.textContent = `Showing ${filtered.length} of ${shortcuts.length} shortcuts`; // Announce to screen readers if (currentSearch) { resultsCount.setAttribute('aria-label', `Found ${filtered.length} shortcuts matching "${currentSearch}"`); } }; // Search handler searchInput.addEventListener('input', (e) => { currentSearch = e.target.value.toLowerCase().trim(); renderShortcuts(); }); // Category filter handlers filterButtons.forEach(btn => { btn.addEventListener('click', () => { filterButtons.forEach(b => { b.classList.remove('active'); b.setAttribute('aria-pressed', 'false'); }); btn.classList.add('active'); btn.setAttribute('aria-pressed', 'true'); currentFilter = btn.dataset.category; renderShortcuts(); }); }); // Initial render renderShortcuts(); // Focus the search input requestAnimationFrame(() => { searchInput.focus(); }); // ESC key closes modal const escHandler = (e) => { if (e.key === 'Escape') { modal.remove(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); }, // v8.38: Enhanced styles for interactive keyboard reference ensureHelpStyles() { if (document.getElementById('keyboard-help-styles')) return; const style = document.createElement('style'); style.id = 'keyboard-help-styles'; style.textContent = ` /* v8.38: Interactive Keyboard Shortcuts Reference Styles */ .keyboard-help-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.9); display: flex; align-items: center; justify-content: center; z-index: 10001; backdrop-filter: blur(8px); animation: fadeIn 0.2s ease-out; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .keyboard-help-content { background: linear-gradient(145deg, rgba(0, 20, 40, 0.98), rgba(0, 10, 30, 0.98)); border: 2px solid rgba(0, 255, 255, 0.4); border-radius: 16px; padding: 30px; max-width: 800px; width: 90%; max-height: 85vh; overflow-y: auto; color: #fff; box-shadow: 0 20px 60px rgba(0, 255, 255, 0.2); animation: slideUp 0.3s ease-out; } @keyframes slideUp { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .keyboard-help-content h2 { color: #0ff; margin-bottom: 25px; font-size: 24px; text-align: center; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 10px rgba(0, 255, 255, 0.5); } /* Search Bar */ .kb-search-container { position: relative; margin-bottom: 20px; } .kb-search-input { width: 100%; padding: 12px 40px 12px 16px; background: rgba(0, 40, 60, 0.6); border: 2px solid rgba(0, 255, 255, 0.3); border-radius: 8px; color: #fff; font-size: 14px; transition: all 0.3s; } .kb-search-input:focus { outline: none; border-color: #0ff; box-shadow: 0 0 15px rgba(0, 255, 255, 0.3); background: rgba(0, 50, 80, 0.8); } .kb-search-input::placeholder { color: #aaa; } .kb-search-icon { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); font-size: 18px; opacity: 0.6; pointer-events: none; } /* Category Filters */ .kb-category-filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; justify-content: center; } .kb-filter-btn { padding: 8px 16px; background: rgba(0, 40, 60, 0.5); border: 1px solid rgba(0, 255, 255, 0.3); border-radius: 20px; color: #aaa; font-size: 12px; cursor: pointer; transition: all 0.3s; text-transform: uppercase; letter-spacing: 1px; font-weight: 500; } .kb-filter-btn:hover { background: rgba(0, 60, 80, 0.7); border-color: #0ff; color: #0ff; transform: translateY(-2px); } .kb-filter-btn.active { background: linear-gradient(135deg, rgba(0, 255, 255, 0.3), rgba(0, 255, 136, 0.3)); border-color: #0ff; color: #0ff; font-weight: bold; box-shadow: 0 4px 12px rgba(0, 255, 255, 0.3); } .kb-filter-btn:focus-visible { outline: 2px solid #0ff; outline-offset: 2px; } /* Shortcuts List */ .kb-shortcuts-list { margin-bottom: 20px; max-height: 400px; overflow-y: auto; padding-right: 10px; } .kb-shortcuts-list::-webkit-scrollbar { width: 8px; } .kb-shortcuts-list::-webkit-scrollbar-track { background: rgba(0, 20, 40, 0.5); border-radius: 4px; } .kb-shortcuts-list::-webkit-scrollbar-thumb { background: rgba(0, 255, 255, 0.3); border-radius: 4px; } .kb-shortcuts-list::-webkit-scrollbar-thumb:hover { background: rgba(0, 255, 255, 0.5); } /* Category Groups */ .kb-category-group { margin-bottom: 20px; animation: fadeInList 0.3s ease-out; } @keyframes fadeInList { from { opacity: 0; transform: translateX(-10px); } to { opacity: 1; transform: translateX(0); } } .kb-category-title { color: #00ff88; font-size: 16px; font-weight: 600; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid rgba(0, 255, 136, 0.3); text-transform: uppercase; letter-spacing: 1.5px; } /* Shortcut Rows */ .kb-row { display: flex; align-items: center; gap: 15px; margin: 8px 0; padding: 8px 12px; background: rgba(0, 40, 60, 0.3); border-radius: 6px; transition: all 0.2s; } .kb-row:hover { background: rgba(0, 60, 80, 0.5); transform: translateX(5px); border-left: 3px solid #0ff; } .kb-key { background: linear-gradient(145deg, rgba(0, 60, 90, 0.8), rgba(0, 40, 70, 0.8)); border: 1px solid rgba(0, 255, 255, 0.4); border-radius: 6px; padding: 6px 12px; font-family: 'Courier New', monospace; font-size: 13px; font-weight: bold; color: #0ff; min-width: 60px; text-align: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.1); text-shadow: 0 0 5px rgba(0, 255, 255, 0.5); } .kb-action { flex: 1; font-size: 13px; color: #ddd; line-height: 1.4; } /* Results Counter */ .kb-results-count { text-align: center; color: #aaa; font-size: 12px; margin-bottom: 15px; padding: 8px; background: rgba(0, 40, 60, 0.4); border-radius: 6px; border: 1px solid rgba(0, 255, 255, 0.2); } /* Close Button */ .keyboard-help-close { display: block; margin: 0 auto; padding: 12px 40px; background: linear-gradient(135deg, #0ff, #00ff88); border: none; border-radius: 25px; color: #000; font-weight: bold; font-size: 14px; cursor: pointer; transition: all 0.3s; box-shadow: 0 4px 15px rgba(0, 255, 255, 0.4); text-transform: uppercase; letter-spacing: 1px; } .keyboard-help-close:hover { transform: scale(1.05) translateY(-2px); box-shadow: 0 6px 25px rgba(0, 255, 255, 0.6); } .keyboard-help-close:active { transform: scale(0.98); } .keyboard-help-close:focus-visible { outline: 3px solid #0ff; outline-offset: 3px; } .kb-hint-text { opacity: 0.7; font-size: 11px; font-weight: normal; } /* Mobile Responsive */ @media (max-width: 600px) { .keyboard-help-content { padding: 20px; max-height: 90vh; } .kb-category-filters { gap: 6px; } .kb-filter-btn { padding: 6px 12px; font-size: 11px; } .kb-key { min-width: 50px; padding: 4px 8px; font-size: 12px; } .kb-action { font-size: 12px; } } `; document.head.appendChild(style); } }; // Initialize keyboard navigation when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => KeyboardNav.init()); } else { KeyboardNav.init(); } window.KeyboardNav = KeyboardNav; // ============================================ // v8.38: GAME STATE ANNOUNCER (8-Strategy Round 6 #3 - 5/8 votes + CRITICAL) // ARIA live announcements for critical game events (accessibility) // Provides screen reader feedback for level ups, achievements, boss spawns, etc. // ============================================ const GameStateAnnouncer = { announcerElement: null, lastAnnouncement: '', announcementQueue: [], isProcessing: false, /** * Initialize the announcer system */ init() { this.announcerElement = document.getElementById('sr-announcements'); if (!this.announcerElement) { Logger.warn('GameStateAnnouncer', 'sr-announcements element not found'); } }, /** * Announce a game state change to screen readers * @param {string} message - The message to announce * @param {string} priority - 'polite' (default) or 'assertive' * @param {number} delay - Optional delay before announcement (ms) */ announce(message, priority = 'polite', delay = 0) { if (!this.announcerElement || !message) return; // Prevent duplicate announcements if (message === this.lastAnnouncement) return; this.lastAnnouncement = message; const doAnnounce = () => { // Set priority level this.announcerElement.setAttribute('aria-live', priority); // Clear and set new message this.announcerElement.textContent = ''; setTimeout(() => { this.announcerElement.textContent = message; }, 50); // Small delay ensures screen readers detect the change // Clear after announcement has been read setTimeout(() => { if (this.announcerElement.textContent === message) { this.announcerElement.textContent = ''; } }, 5000); Logger.info('GameStateAnnouncer', `Announced: "${message}"`); }; if (delay > 0) { setTimeout(doAnnounce, delay); } else { doAnnounce(); } }, /** * Announce skill level up */ announceSkillLevelUp(skill, newLevel) { const skillName = skill.charAt(0).toUpperCase() + skill.slice(1); this.announce(`${skillName} skill leveled up to level ${newLevel}!`, 'polite'); }, /** * Announce achievement unlock */ announceAchievement(achievementName) { this.announce(`Achievement unlocked: ${achievementName}!`, 'assertive'); }, /** * Announce combat victory */ announceVictory(enemyName) { this.announce(`Victory! Defeated ${enemyName}.`, 'polite'); }, /** * Announce boss spawn */ announceBossSpawn(bossName) { this.announce(`Warning! ${bossName} has appeared!`, 'assertive'); }, /** * Announce wave completion */ announceWaveComplete(waveNumber) { this.announce(`Wave ${waveNumber} completed!`, 'polite'); }, /** * Announce player defeat */ announceDefeat() { this.announce('You have been defeated. Respawning...', 'assertive'); }, /** * Announce critical health */ announceLowHealth() { // Only announce once per combat encounter if (!this._lowHealthAnnounced) { this.announce('Warning: Health is critically low!', 'assertive'); this._lowHealthAnnounced = true; // Reset flag after 10 seconds setTimeout(() => { this._lowHealthAnnounced = false; }, 10000); } }, /** * Announce item acquired */ announceItemAcquired(itemName, quantity = 1) { const quantityText = quantity > 1 ? ` (${quantity})` : ''; this.announce(`Acquired ${itemName}${quantityText}`, 'polite'); }, /** * Announce quest completion */ announceQuestComplete(questName) { this.announce(`Quest completed: ${questName}!`, 'polite'); }, /** * Announce daily challenge completion */ announceDailyChallengeComplete() { this.announce('Daily challenge completed! Bonus experience earned.', 'polite'); }, /** * Announce P2P connection status */ announceP2PStatus(status, details = '') { const messages = { 'connected': `Connected to multiplayer session. ${details}`, 'disconnected': 'Disconnected from multiplayer session.', 'host': 'You are now hosting a multiplayer session.', 'spectator': `Spectator mode activated. Following ${details}.` }; this.announce(messages[status] || status, 'polite'); }, /** * Clear the announcement region */ clear() { if (this.announcerElement) { this.announcerElement.textContent = ''; } this.lastAnnouncement = ''; } }; // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => GameStateAnnouncer.init()); } else { GameStateAnnouncer.init(); } window.GameStateAnnouncer = GameStateAnnouncer; // ============================================ // v7.3: SCENE DISPOSAL SYSTEM (8-Strategy Consensus Cycle 1) // Properly disposes Three.js geometries, materials, and textures // to prevent GPU memory leaks during mode transitions // ============================================ const SceneDisposal = { // Recursively dispose all resources in a Three.js object disposeObject(obj) { if (!obj) return; // Recursively handle children first while (obj.children && obj.children.length > 0) { this.disposeObject(obj.children[0]); obj.remove(obj.children[0]); } // Dispose geometry if (obj.geometry) { obj.geometry.dispose(); } // Dispose materials if (obj.material) { if (Array.isArray(obj.material)) { obj.material.forEach(mat => this.disposeMaterial(mat)); } else { this.disposeMaterial(obj.material); } } }, // Dispose a single material and its textures disposeMaterial(material) { if (!material) return; // Dispose all texture types const textureProps = ['map', 'normalMap', 'bumpMap', 'specularMap', 'emissiveMap', 'alphaMap', 'aoMap', 'envMap', 'lightMap', 'displacementMap', 'roughnessMap', 'metalnessMap']; textureProps.forEach(prop => { if (material[prop]) { material[prop].dispose(); } }); material.dispose(); }, // Clear entire scene with proper disposal clearScene(sceneObj) { if (!sceneObj) return; const toDispose = [...sceneObj.children]; toDispose.forEach(child => { this.disposeObject(child); sceneObj.remove(child); }); debugLog('SceneDisposal', 'Scene cleared with resource disposal'); // v8.25: gated } }; // ============================================ // v7.3: AUTO-SAVE SYSTEM WITH BACKUP ROTATION (8-Strategy Consensus Cycle 1) // Periodic saves with write-ahead backup to prevent data loss // ============================================ const AutoSaveSystem = { INTERVAL: 30000, // 30 seconds MAX_BACKUPS: 2, // Keep 2 backup generations lastSaveTime: 0, initialized: false, init() { if (this.initialized) return; // v8.39: Save on visibility change using centralized manager PageVisibilityManager.subscribe('autoSave', (isVisible) => { if (!isVisible && typeof saveGameData === 'function') { this.saveWithBackup(); } }); // Save on beforeunload window.addEventListener('beforeunload', () => { if (typeof saveGameData === 'function') { this.saveWithBackup(); } }); this.initialized = true; debugLog('AutoSaveSystem', 'Initialized with', this.INTERVAL / 1000, 'second interval'); // v8.25: gated }, // Check and trigger periodic save (call from game loop) update(currentTime) { if (!this.initialized) this.init(); if (currentTime - this.lastSaveTime >= this.INTERVAL) { this.saveWithBackup(); this.lastSaveTime = currentTime; } }, // Save with backup rotation saveWithBackup() { if (typeof APP_NAME === 'undefined' || typeof gameData === 'undefined') return; try { // Rotate existing backups before saving const currentData = localStorage.getItem(APP_NAME); if (currentData) { // Move backup-1 to backup-2 const backup1 = localStorage.getItem(APP_NAME + '-backup-1'); if (backup1 && this.MAX_BACKUPS >= 2) { localStorage.setItem(APP_NAME + '-backup-2', backup1); } // Move current to backup-1 localStorage.setItem(APP_NAME + '-backup-1', currentData); } } catch (e) { debugWarn('AutoSaveSystem', 'Backup rotation failed:', e); // v8.25: gated } }, // Attempt recovery from backup // v8.0: Now using SafeJSON utility (8-Strategy Consensus Cycle 3) recoverFromBackup() { for (let i = 1; i <= this.MAX_BACKUPS; i++) { try { const backup = localStorage.getItem(APP_NAME + '-backup-' + i); if (backup) { const parsed = SafeJSON.parse(backup, null, { repair: true, log: true }); if (parsed && parsed.version) { debugLog('AutoSaveSystem', 'Recovered from backup-' + i); // v8.25: gated return parsed; } } } catch (e) { continue; } } return null; } }; // ============================================ // v7.4: ENHANCED DAMAGE NUMBER SYSTEM (8-Strategy Consensus Cycle 2) // Visual variety for different damage types with elemental styling // ============================================ const DamageNumberStyles = { styles: { normal: { color: '#ffffff', scale: 1.0, prefix: '', suffix: '', glow: null }, critical: { color: '#ffdd00', scale: 1.6, prefix: '\u26A1 ', suffix: '!', glow: '0 0 15px #ffdd00', shake: true }, fire: { color: '#ff6622', scale: 1.15, prefix: '\uD83D\uDD25 ', suffix: '', glow: '0 0 12px #ff4400' }, ice: { color: '#88ddff', scale: 1.1, prefix: '\u2744\uFE0F ', suffix: '', glow: '0 0 10px #00aaff' }, lightning: { color: '#ffff88', scale: 1.2, prefix: '\u26A1 ', suffix: '', glow: '0 0 18px #ffff00', flicker: true }, poison: { color: '#88ff44', scale: 0.9, prefix: '\u2620\uFE0F ', suffix: '', glow: '0 0 8px #44ff00' }, void: { color: '#cc44ff', scale: 1.15, prefix: '\uD83C\uDF00 ', suffix: '', glow: '0 0 15px #8800ff' }, heal: { color: '#44ff88', scale: 1.0, prefix: '+', suffix: '', glow: '0 0 10px #00ff44' }, overkill: { color: '#ff0088', scale: 2.0, prefix: '\uD83D\uDC80 OVERKILL ', suffix: ' \uD83D\uDC80', glow: '0 0 25px #ff00ff', screenShake: 0.4 }, parry: { color: '#00ffff', scale: 1.4, prefix: '\uD83D\uDEE1\uFE0F ', suffix: '', glow: '0 0 12px #00ffff' }, block: { color: '#aaaaaa', scale: 0.8, prefix: '', suffix: ' (blocked)', glow: null } }, spawn(position, damage, type = 'normal', options = {}) { if (typeof spawnFloater !== 'function') return null; const style = this.styles[type] || this.styles.normal; const displayDamage = typeof damage === 'number' ? Math.round(damage) : damage; const text = `${style.prefix}${displayDamage}${style.suffix}`; const isCrit = type === 'critical' || type === 'overkill'; // Create the floater const floaterResult = spawnFloater(position, text, style.color, isCrit); // Apply additional styling via post-processing if (style.glow) { this.applyGlowEffect(style.glow); } // Lightning flicker effect if (style.flicker) { this.applyFlickerEffect(); } // Screen shake for overkill if (style.screenShake && typeof screenShake === 'function') { screenShake(style.screenShake); } // Spawn supporting particles for certain types if (typeof particles !== 'undefined' && particles && position) { const particleColors = { fire: 0xff4400, ice: 0x88ddff, lightning: 0xffff00, poison: 0x88ff44, void: 0xcc44ff, overkill: 0xff0088, critical: 0xffdd00 }; if (particleColors[type]) { particles.emit(position, type === 'overkill' ? 15 : 5, particleColors[type]); } } return floaterResult; }, applyGlowEffect(glowStyle) { // Find the most recently added floater and apply glow const floaters = document.querySelectorAll('.floater'); if (floaters.length > 0) { const latest = floaters[floaters.length - 1]; if (latest && latest.style) { latest.style.textShadow = glowStyle + ', -1px -1px 0 #000, 1px 1px 0 #000'; } } }, applyFlickerEffect() { const floaters = document.querySelectorAll('.floater'); if (floaters.length > 0) { const latest = floaters[floaters.length - 1]; if (latest) { let flickerCount = 0; const flickerInterval = setInterval(() => { latest.style.opacity = Math.random() > 0.3 ? '1' : '0.5'; flickerCount++; if (flickerCount > 6) { clearInterval(flickerInterval); latest.style.opacity = '1'; } }, 60); } } }, // Detect damage type from context detectType(damage, options = {}) { if (options.type) return options.type; if (options.isHeal) return 'heal'; if (options.isParry) return 'parry'; if (options.isBlock) return 'block'; if (options.isCrit) return 'critical'; if (options.element === 'fire' || options.element === 'Fire') return 'fire'; if (options.element === 'ice' || options.element === 'Ice') return 'ice'; if (options.element === 'lightning' || options.element === 'Lightning') return 'lightning'; if (options.element === 'poison' || options.element === 'Poison') return 'poison'; if (options.element === 'void' || options.element === 'Void') return 'void'; if (options.isOverkill) return 'overkill'; return 'normal'; } }; // ============================================ // v7.4: QUICK TRAVEL PANEL (8-Strategy Consensus Cycle 2) // Fast navigation to previously visited planets // ============================================ const QuickTravelSystem = { modalId: 'quick-travel-modal', isOpen: false, focusTrapId: null, // v7.79: Track focus trap open() { if (!document.getElementById(this.modalId)) { this.createModal(); } const modal = document.getElementById(this.modalId); if (modal) { modal.classList.add('active'); this.isOpen = true; this.renderPlanetList(); // v7.79: Set up focus trap const modalContent = modal.querySelector('.modal-content'); if (modalContent && typeof FocusTrap !== 'undefined') { const search = document.getElementById('quick-travel-search'); this.focusTrapId = FocusTrap.create(modalContent, { initialFocus: search, onEscape: () => this.close() }); } else { // Fallback: Focus search input const search = document.getElementById('quick-travel-search'); if (search) search.focus(); } } }, close() { const modal = document.getElementById(this.modalId); if (modal) { modal.classList.remove('active'); this.isOpen = false; // v7.79: Destroy focus trap if (this.focusTrapId && typeof FocusTrap !== 'undefined') { FocusTrap.destroy(this.focusTrapId); this.focusTrapId = null; } } }, toggle() { if (this.isOpen) { this.close(); } else { this.open(); } }, createModal() { const modal = document.createElement('div'); modal.id = this.modalId; modal.className = 'modal-overlay'; // v7.78: Added ARIA attributes for accessibility modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-labelledby', 'quick-travel-title'); modal.innerHTML = ` `; // Close on backdrop click modal.addEventListener('click', (e) => { if (e.target === modal) this.close(); }); document.body.appendChild(modal); }, renderPlanetList(filter = '') { const list = document.getElementById('quick-travel-list'); if (!list) return; const visitedIds = (typeof gameData !== 'undefined' && gameData.visitedPlanets) ? gameData.visitedPlanets : []; if (visitedIds.length === 0) { list.innerHTML = '
No planets visited yet.
Explore the galaxy to unlock quick travel!
'; return; } let html = ''; const filterLower = filter.toLowerCase(); let matchCount = 0; visitedIds.forEach(civId => { const civ = (typeof civs !== 'undefined') ? civs[civId] : null; if (!civ) return; const name = civ.name || ('Planet ' + civId); if (filter && !name.toLowerCase().includes(filterLower)) return; matchCount++; const isCurrentPlanet = (typeof mode !== 'undefined' && mode === 'world' && typeof currentCiv !== 'undefined' && currentCiv && currentCiv.id === civId); const threatColor = this.getThreatColor(civ.threatLevel); html += `
${civ.icon || '\uD83C\uDF0D'} ${name}
Threat: ${civ.threatLevel || 'Unknown'}
${isCurrentPlanet ? '\uD83D\uDCCD Here' : '\u2192' }
`; }); if (matchCount === 0 && filter) { list.innerHTML = '
No planets match your search.
'; } else { list.innerHTML = html; } }, filterPlanets(searchTerm) { this.renderPlanetList(searchTerm); }, getThreatColor(threat) { const threatNum = parseInt(threat) || 0; if (threatNum <= 2) return '#4a4'; if (threatNum <= 4) return '#aa4'; if (threatNum <= 6) return '#a84'; if (threatNum <= 8) return '#a44'; return '#f44'; }, travelTo(civId) { this.close(); const civ = (typeof civs !== 'undefined') ? civs[civId] : null; if (!civ) { if (typeof showNotification === 'function') { showNotification('Planet not found!', 'error'); } return; } const planetName = civ.name || ('Planet ' + civId); if (typeof showNotification === 'function') { showNotification('\uD83D\uDE80 Quick traveling to ' + planetName + '...', 'info'); } // Trigger warp effect and travel setTimeout(() => { if (typeof showWarpEffect === 'function') { showWarpEffect(() => { if (typeof selectCiv === 'function') { selectCiv(civId); } }); } else if (typeof selectCiv === 'function') { selectCiv(civId); } }, 200); } }; // ============================================ // v7.4: MOBILE HAPTIC FEEDBACK SYSTEM (8-Strategy Consensus Cycle 2) // Tactile feedback for touch controls using Vibration API // ============================================ const MobileHaptics = { enabled: true, supported: typeof navigator !== 'undefined' && 'vibrate' in navigator, // Pattern library (durations in ms) patterns: { tap: [12], // Light UI tap attack: [25], // Quick attack pulse heavyAttack: [40, 15, 40], // Heavy attack double pulse damage: [60, 25, 80], // Taking damage dodge: [15, 12, 15], // Quick triple for dodge parry: [20, 10, 50], // Successful parry abilityUse: [25, 15, 35], // Ability activation levelUp: [40, 25, 40, 25, 80], // Celebration pattern lowHealth: [100, 40, 100], // Warning pattern loot: [15, 15, 15, 15], // Collection feedback criticalHit: [70, 20, 70], // Big impact death: [150, 50, 100, 50, 200], // Death rumble warp: [30, 20, 30, 20, 30, 20, 60], // Warp travel menuOpen: [8], // Menu interaction error: [80, 40, 80] // Error feedback }, vibrate(patternName) { if (!this.enabled || !this.supported) return false; const pattern = this.patterns[patternName]; if (!pattern) { debugWarn('MobileHaptics', 'Unknown pattern:', patternName); // v8.25: gated return false; } try { navigator.vibrate(pattern); return true; } catch (e) { // Silently fail - haptics are optional return false; } }, // Custom vibration with explicit pattern vibrateCustom(pattern) { if (!this.enabled || !this.supported) return false; try { navigator.vibrate(pattern); return true; } catch (e) { return false; } }, // Stop any ongoing vibration stop() { if (this.supported) { try { navigator.vibrate(0); } catch (e) {} } }, // Toggle haptics on/off toggle(enabled) { this.enabled = enabled !== undefined ? enabled : !this.enabled; if (this.enabled && this.supported) { this.vibrate('tap'); // Confirmation feedback } // Persist preference try { localStorage.setItem('leviathan_haptics', this.enabled ? '1' : '0'); } catch (e) {} return this.enabled; }, // Initialize from saved preference init() { try { const saved = localStorage.getItem('leviathan_haptics'); if (saved !== null) { this.enabled = saved === '1'; } } catch (e) {} debugLog('MobileHaptics', 'Initialized - Supported:', this.supported, 'Enabled:', this.enabled); // v8.25: gated } }; // Initialize haptics on load MobileHaptics.init(); // ============================================ // v8.27: VISUAL FEEDBACK SYSTEM // Screen effects for impact, success, and state changes // v8.28: Enhanced with Easing functions for smoother decay // ============================================ const VisualFeedback = { // Screen shake effect // v8.28: Uses Easing.easeOutQuart for natural decay curve shake(intensity = 5, duration = 200) { const container = document.getElementById('container'); if (!container) return; const startTime = performance.now(); const originalTransform = container.style.transform; const animate = () => { // v8.34: Skip animation when tab is hidden if (!isPageVisible) { requestAnimationFrame(animate); return; } const elapsed = performance.now() - startTime; if (elapsed >= duration) { container.style.transform = originalTransform; return; } // v8.28: Use Easing for smoother decay curve const progress = elapsed / duration; const decay = typeof Easing !== 'undefined' ? 1 - Easing.easeOutQuart(progress) : 1 - progress; const x = (Math.random() - 0.5) * intensity * decay * 2; const y = (Math.random() - 0.5) * intensity * decay * 2; container.style.transform = `translate(${x}px, ${y}px)`; requestAnimationFrame(animate); }; requestAnimationFrame(animate); }, // Screen flash effect flash(color = 'rgba(255, 255, 255, 0.3)', duration = 150) { const flash = document.createElement('div'); flash.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: ${color}; pointer-events: none; z-index: 99999; animation: fadeOut ${duration}ms ease-out forwards; `; // Add keyframe animation if not exists if (!document.getElementById('visual-feedback-styles')) { const style = document.createElement('style'); style.id = 'visual-feedback-styles'; style.textContent = ` @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } @keyframes pulseGlow { 0%, 100% { box-shadow: inset 0 0 30px rgba(0,255,136,0.3); } 50% { box-shadow: inset 0 0 60px rgba(0,255,136,0.6); } } @keyframes borderPulse { 0%, 100% { border-color: rgba(0,255,136,0.5); } 50% { border-color: rgba(0,255,136,1); } } `; document.head.appendChild(style); } document.body.appendChild(flash); setTimeout(() => flash.remove(), duration); }, // Damage indicator (red vignette) damageVignette(intensity = 0.5, duration = 400) { this.flash(`radial-gradient(ellipse at center, transparent 40%, rgba(255, 0, 0, ${intensity}) 100%)`, duration); }, // Heal indicator (green vignette) healVignette(intensity = 0.3, duration = 300) { this.flash(`radial-gradient(ellipse at center, transparent 50%, rgba(0, 255, 100, ${intensity}) 100%)`, duration); }, // Level up / success burst successBurst(color = '#0f0') { this.flash(`radial-gradient(circle at center, ${color}40 0%, transparent 70%)`, 400); }, // Ripple effect at a point ripple(x, y, color = 'rgba(255, 255, 255, 0.5)', size = 100) { const ripple = document.createElement('div'); ripple.style.cssText = ` position: fixed; left: ${x}px; top: ${y}px; width: 0; height: 0; border-radius: 50%; background: ${color}; pointer-events: none; z-index: 99998; transform: translate(-50%, -50%); animation: rippleExpand 0.5s ease-out forwards; `; // Add ripple animation if not exists if (!document.getElementById('ripple-styles')) { const style = document.createElement('style'); style.id = 'ripple-styles'; style.textContent = ` @keyframes rippleExpand { from { width: 0; height: 0; opacity: 1; } to { width: ${size * 2}px; height: ${size * 2}px; opacity: 0; } } `; document.head.appendChild(style); } document.body.appendChild(ripple); setTimeout(() => ripple.remove(), TIMING.RIPPLE_DURATION); // v8.38: Using timing constants }, // Combined feedback for common actions onHit() { this.shake(3, 100); if (typeof MobileHaptics !== 'undefined') MobileHaptics.vibrate('tap'); }, onCriticalHit() { this.shake(8, 200); this.flash('rgba(255, 200, 0, 0.2)', 150); if (typeof MobileHaptics !== 'undefined') MobileHaptics.vibrate('heavyTap'); }, onDeath() { this.shake(15, 500); this.damageVignette(0.7, 800); if (typeof MobileHaptics !== 'undefined') MobileHaptics.vibrate('death'); }, onLevelUp() { this.successBurst('#00ff88'); if (typeof MobileHaptics !== 'undefined') MobileHaptics.vibrate('levelUp'); if (typeof UISoundSystem !== 'undefined') UISoundSystem.play('levelUp'); }, // v8.29: Achievement unlock celebration onAchievement() { this.successBurst('#ffd700'); // Gold burst for achievements this.shake(2, 100); if (typeof MobileHaptics !== 'undefined') MobileHaptics.vibrate('heavyTap'); if (typeof UISoundSystem !== 'undefined') UISoundSystem.play('discover'); }, // v8.29: Critical event feedback (boss spawn, major discovery) onCriticalEvent(color = '#ff00ff') { this.successBurst(color); this.shake(5, 300); if (typeof MobileHaptics !== 'undefined') MobileHaptics.vibrate('heavyTap'); } }; // Make globally accessible window.VisualFeedback = VisualFeedback; // ============================================ // v8.30: ENHANCED TOOLTIP SYSTEM // Rich, informative tooltips with stats, hotkeys, and descriptions // ============================================ const EnhancedTooltip = { element: null, hideTimeout: null, showDelay: 400, // ms before showing showTimeout: null, // Create tooltip element if it doesn't exist init() { if (this.element) return; this.element = document.createElement('div'); this.element.className = 'enhanced-tooltip'; this.element.id = 'enhanced-tooltip'; this.element.setAttribute('role', 'tooltip'); this.element.setAttribute('aria-hidden', 'true'); document.body.appendChild(this.element); // Global hover listeners for elements with data-tooltip document.addEventListener('mouseover', (e) => this.handleMouseOver(e)); document.addEventListener('mouseout', (e) => this.handleMouseOut(e)); document.addEventListener('mousemove', (e) => this.handleMouseMove(e)); debugLog('EnhancedTooltip', 'Initialized'); }, // Handle mouse over events handleMouseOver(e) { const target = e.target.closest('[data-tooltip]'); if (!target) return; // Clear any pending hide if (this.hideTimeout) { clearTimeout(this.hideTimeout); this.hideTimeout = null; } // Delay showing tooltip this.showTimeout = setTimeout(() => { this.show(target, e); }, this.showDelay); }, // Handle mouse out events handleMouseOut(e) { const target = e.target.closest('[data-tooltip]'); if (!target) { // Clear pending show if (this.showTimeout) { clearTimeout(this.showTimeout); this.showTimeout = null; } return; } // Cancel pending show if (this.showTimeout) { clearTimeout(this.showTimeout); this.showTimeout = null; } // Delay hiding tooltip this.hideTimeout = setTimeout(() => { this.hide(); }, 100); }, // Handle mouse move for positioning handleMouseMove(e) { if (!this.element.classList.contains('visible')) return; this.position(e.clientX, e.clientY); }, // Show tooltip for an element show(target, e) { const tooltipData = target.dataset.tooltip; const title = target.dataset.tooltipTitle || target.getAttribute('aria-label') || ''; const desc = target.dataset.tooltipDesc || ''; const hotkey = target.dataset.tooltipHotkey || ''; const icon = target.dataset.tooltipIcon || ''; const stats = target.dataset.tooltipStats || ''; // Build tooltip HTML let html = ''; if (title) { html += `
`; if (icon) html += `${icon}`; html += `${this.escapeHtml(title)}`; if (hotkey) html += `${hotkey}`; html += `
`; } if (tooltipData || desc) { html += `
${this.escapeHtml(tooltipData || desc)}
`; } if (stats) { try { const statsObj = JSON.parse(stats); html += `
`; for (const [key, value] of Object.entries(statsObj)) { html += `
${key}:${value}
`; } html += `
`; } catch (err) { // Stats not valid JSON, ignore } } if (!html) return; // Nothing to show this.element.innerHTML = html; this.element.setAttribute('aria-hidden', 'false'); this.position(e.clientX, e.clientY); this.element.classList.add('visible'); }, // Hide tooltip hide() { this.element.classList.remove('visible'); this.element.setAttribute('aria-hidden', 'true'); }, // Position tooltip near cursor position(x, y) { const padding = 15; const rect = this.element.getBoundingClientRect(); let left = x + padding; let top = y + padding; // Keep within viewport if (left + rect.width > window.innerWidth - padding) { left = x - rect.width - padding; } if (top + rect.height > window.innerHeight - padding) { top = y - rect.height - padding; } // Ensure not negative left = Math.max(padding, left); top = Math.max(padding, top); this.element.style.left = `${left}px`; this.element.style.top = `${top}px`; }, // Escape HTML to prevent XSS escapeHtml(text) { if (!text) return ''; return String(text) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } }; // Make globally accessible window.EnhancedTooltip = EnhancedTooltip; // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => EnhancedTooltip.init()); } else { EnhancedTooltip.init(); } // ============================================ // v7.5: UI Sound Feedback System (8-Strategy Consensus Cycle 3) // Comprehensive audio feedback for UI interactions // Uses therapeutic pentatonic scale design philosophy // ============================================ const UISoundSystem = { enabled: true, volume: 0.3, audioContext: null, masterGain: null, // Sound definitions using pentatonic frequencies for pleasant tones sounds: { hover: { freq: 880, duration: 0.04, type: 'sine', gain: 0.15 }, click: { freq: 660, duration: 0.08, type: 'triangle', gain: 0.25 }, open: { freqs: [440, 554, 660], duration: 0.15, type: 'sine', gain: 0.2, stagger: 0.04 }, close: { freqs: [660, 554, 440], duration: 0.12, type: 'sine', gain: 0.18, stagger: 0.03 }, tab: { freq: 523, duration: 0.06, type: 'triangle', gain: 0.2 }, toggleOn: { freqs: [392, 523], duration: 0.1, type: 'sine', gain: 0.22, stagger: 0.05 }, toggleOff: { freqs: [523, 392], duration: 0.1, type: 'sine', gain: 0.18, stagger: 0.05 }, success: { freqs: [523, 659, 784], duration: 0.2, type: 'sine', gain: 0.25, stagger: 0.08 }, warning: { freqs: [440, 440], duration: 0.15, type: 'square', gain: 0.15, stagger: 0.12 }, error: { freqs: [220, 196], duration: 0.2, type: 'sawtooth', gain: 0.18, stagger: 0.1 }, pickup: { freqs: [660, 880, 1047], duration: 0.12, type: 'sine', gain: 0.2, stagger: 0.03 }, equip: { freq: 392, duration: 0.15, type: 'triangle', gain: 0.22, detune: 1200 }, levelUp: { freqs: [523, 659, 784, 1047], duration: 0.4, type: 'sine', gain: 0.28, stagger: 0.1 }, navigate: { freq: 740, duration: 0.05, type: 'sine', gain: 0.12 }, confirm: { freqs: [587, 784], duration: 0.12, type: 'triangle', gain: 0.22, stagger: 0.06 }, cancel: { freq: 294, duration: 0.1, type: 'triangle', gain: 0.18 }, notification: { freqs: [784, 988, 784], duration: 0.25, type: 'sine', gain: 0.2, stagger: 0.08 }, craft: { freqs: [330, 440, 554, 660], duration: 0.3, type: 'triangle', gain: 0.22, stagger: 0.06 }, discover: { freqs: [440, 554, 659, 880, 1047], duration: 0.5, type: 'sine', gain: 0.25, stagger: 0.09 } }, // Initialize audio context lazily (requires user interaction) init() { if (this.audioContext) return true; // v7.29: Use shared AudioContext (Cycle 2 Consensus) this.audioContext = typeof getSharedAudioContext === 'function' ? getSharedAudioContext() : new (window.AudioContext || window.webkitAudioContext)(); if (!this.audioContext) { debugWarn('UISoundSystem', 'No AudioContext available'); // v8.25: gated return false; } this.masterGain = this.audioContext.createGain(); this.masterGain.gain.value = this.volume; this.masterGain.connect(this.audioContext.destination); // Load saved preferences // v8.25: Wrapped in try/catch for localStorage safety try { const savedEnabled = localStorage.getItem('leviathan_ui_sounds'); if (savedEnabled !== null) { this.enabled = savedEnabled === '1'; } const savedVolume = localStorage.getItem('leviathan_ui_volume'); if (savedVolume !== null) { const parsed = parseFloat(savedVolume); if (isFinite(parsed)) { this.volume = Math.max(0, Math.min(1, parsed)); // v8.25: Clamp volume this.masterGain.gain.value = this.volume; } } } catch (e) { /* localStorage may be unavailable in private mode */ } debugLog('UISoundSystem', 'Using shared AudioContext - Enabled:', this.enabled, 'Volume:', this.volume); // v8.25: gated return true; }, // Resume audio context if suspended (iOS/Safari requirement) async resume() { if (this.audioContext && this.audioContext.state === 'suspended') { await this.audioContext.resume(); } }, // Play a UI sound by name play(soundName) { if (!this.enabled) return; if (!this.init()) return; const sound = this.sounds[soundName]; if (!sound) { debugWarn('UISoundSystem', 'Unknown sound:', soundName); // v8.25: gated return; } this.resume(); // v8.35: Trigger visual sound indicator (8-Strategy Round 3 #2) // Accessibility: Show visual feedback for deaf/hard-of-hearing users this.triggerVisualIndicator(soundName); try { if (sound.freqs) { // Multi-tone sound (arpeggio) sound.freqs.forEach((freq, i) => { setTimeout(() => { this.playTone(freq, sound.duration, sound.type, sound.gain, sound.detune); }, i * (sound.stagger || 0.05) * 1000); }); } else { // Single tone this.playTone(sound.freq, sound.duration, sound.type, sound.gain, sound.detune); } } catch (e) { debugWarn('UISoundSystem', 'Error playing sound:', e); // v8.25: gated } }, // v8.35: Visual sound indicator for accessibility (8-Strategy Round 3 #2) triggerVisualIndicator(soundName) { const indicator = document.getElementById('visual-sound-indicator'); if (!indicator) return; // Determine color based on sound type indicator.className = 'visual-sound-indicator flash'; if (['error', 'warning'].includes(soundName)) { indicator.classList.add('flash-alert'); } else if (['pickup', 'craft', 'discover', 'levelUp'].includes(soundName)) { indicator.classList.add('flash-sfx'); } else { indicator.classList.add('flash-ui'); } // Reset animation by removing and re-adding class indicator.style.animation = 'none'; requestAnimationFrame(() => { indicator.style.animation = ''; }); }, // Play a single tone playTone(freq, duration, type = 'sine', gain = 0.2, detune = 0) { const ctx = this.audioContext; const now = ctx.currentTime; const osc = ctx.createOscillator(); const gainNode = ctx.createGain(); osc.type = type; osc.frequency.value = freq; if (detune) osc.detune.value = detune; // ADSR envelope for smooth sound gainNode.gain.setValueAtTime(0, now); gainNode.gain.linearRampToValueAtTime(gain, now + 0.01); // Attack gainNode.gain.linearRampToValueAtTime(gain * 0.7, now + duration * 0.3); // Decay gainNode.gain.linearRampToValueAtTime(gain * 0.5, now + duration * 0.8); // Sustain gainNode.gain.linearRampToValueAtTime(0, now + duration); // Release osc.connect(gainNode); gainNode.connect(this.masterGain); osc.start(now); osc.stop(now + duration + 0.05); }, // Set volume (0-1) setVolume(vol) { this.volume = Math.max(0, Math.min(1, vol)); if (this.masterGain) { this.masterGain.gain.value = this.volume; } try { localStorage.setItem('leviathan_ui_volume', this.volume.toString()); } catch (e) {} }, // Toggle sounds on/off toggle(enabled) { if (typeof enabled === 'boolean') { this.enabled = enabled; } else { this.enabled = !this.enabled; } try { localStorage.setItem('leviathan_ui_sounds', this.enabled ? '1' : '0'); } catch (e) {} return this.enabled; }, // Attach to common UI elements automatically // v6.77: Fixed TypeError when e.target is a Text node (8-strategy consensus) attachToUI() { // Hover sounds for buttons // Use optional chaining to handle Text nodes that don't have .matches() document.addEventListener('mouseenter', (e) => { if (e.target?.matches?.('button, .btn, [role="button"], .menu-item, .tab')) { this.play('hover'); } }, true); // Click sounds // Use optional chaining to handle Text nodes that don't have .matches() document.addEventListener('click', (e) => { if (e.target?.matches?.('button, .btn, [role="button"]')) { this.play('click'); } else if (e.target?.matches?.('.tab, [role="tab"]')) { this.play('tab'); } else if (e.target?.matches?.('input[type="checkbox"], input[type="radio"]')) { this.play(e.target.checked ? 'toggleOn' : 'toggleOff'); } }, true); debugLog('UISoundSystem', 'Attached to UI elements'); // v8.25: gated } }; // Initialize UI sounds on first user interaction document.addEventListener('click', function initUISound() { UISoundSystem.init(); UISoundSystem.attachToUI(); document.removeEventListener('click', initUISound); }, { once: true }); // ============================================ // v7.5: Color Blindness Support Modes (8-Strategy Consensus Cycle 3) // Accessibility feature with user-selectable color palettes // Supports protanopia, deuteranopia, tritanopia, and high contrast // ============================================ const ColorBlindnessSupport = { currentMode: 'normal', // Color palette mappings for different vision types modes: { normal: { name: 'Normal Vision', description: 'Default color scheme', colors: { damage: '#ff4444', heal: '#44ff44', fire: '#ff6622', ice: '#44ccff', lightning: '#ffee44', poison: '#88ff44', void: '#aa44ff', critical: '#ffdd00', friendly: '#44ff88', enemy: '#ff4466', neutral: '#aaaaaa', rare: '#4488ff', epic: '#aa44ff', legendary: '#ffaa00', mythic: '#ff44aa' } }, protanopia: { name: 'Protanopia Mode', description: 'Red-blind friendly (blue/yellow emphasis)', colors: { damage: '#0066cc', // Blue instead of red heal: '#ffee00', // Yellow instead of green fire: '#ff9900', // Orange stays visible ice: '#00ccff', // Cyan lightning: '#ffffff', // White poison: '#ccff00', // Yellow-green void: '#9966ff', // Blue-purple critical: '#ffffff', // White flash friendly: '#00ffcc', // Cyan-green enemy: '#0066cc', // Blue neutral: '#888888', // Gray rare: '#00aaff', // Bright blue epic: '#cc66ff', // Light purple legendary: '#ffcc00', // Gold mythic: '#ff66cc' // Pink } }, deuteranopia: { name: 'Deuteranopia Mode', description: 'Green-blind friendly (blue/yellow emphasis)', colors: { damage: '#ff6600', // Orange-red heal: '#00ccff', // Cyan instead of green fire: '#ff4400', // Orange-red ice: '#00aaff', // Blue lightning: '#ffff00', // Yellow poison: '#00ffff', // Cyan void: '#aa00ff', // Purple critical: '#ffff00', // Yellow friendly: '#00ddff', // Cyan enemy: '#ff6600', // Orange neutral: '#999999', // Gray rare: '#0088ff', // Blue epic: '#bb00ff', // Purple legendary: '#ffaa00', // Orange-gold mythic: '#ff00aa' // Magenta } }, tritanopia: { name: 'Tritanopia Mode', description: 'Blue-blind friendly (red/green emphasis)', colors: { damage: '#ff0044', // Red heal: '#00ff44', // Green fire: '#ff4400', // Red-orange ice: '#ff88aa', // Pink instead of blue lightning: '#ffcc00', // Orange-yellow poison: '#88ff00', // Yellow-green void: '#ff0088', // Magenta critical: '#ff8800', // Orange friendly: '#00ff88', // Green enemy: '#ff0066', // Red-pink neutral: '#aaaaaa', // Gray rare: '#ff6688', // Pink epic: '#ff0088', // Magenta legendary: '#ffaa00', // Orange mythic: '#ff4488' // Red-pink } }, highContrast: { name: 'High Contrast', description: 'Maximum visibility with stark contrasts', colors: { damage: '#ff0000', // Pure red heal: '#00ff00', // Pure green fire: '#ff8800', // Orange ice: '#00ffff', // Cyan lightning: '#ffff00', // Yellow poison: '#00ff88', // Mint void: '#ff00ff', // Magenta critical: '#ffffff', // White friendly: '#00ff00', // Green enemy: '#ff0000', // Red neutral: '#ffffff', // White rare: '#00aaff', // Blue epic: '#ff00ff', // Magenta legendary: '#ffff00', // Yellow mythic: '#00ffff' // Cyan } } }, // Get a color from current mode getColor(colorName) { const mode = this.modes[this.currentMode] || this.modes.normal; return mode.colors[colorName] || this.modes.normal.colors[colorName] || '#ffffff'; }, // Set color blindness mode setMode(modeName) { if (!this.modes[modeName]) { debugWarn('ColorBlindnessSupport', 'Unknown mode:', modeName); // v8.25: gated return false; } this.currentMode = modeName; this.applyCSS(); try { localStorage.setItem('leviathan_colorblind_mode', modeName); } catch (e) {} debugLog('ColorBlindnessSupport', 'Mode set to:', this.modes[modeName].name); // v8.25: gated // Trigger UI sound if available if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('confirm'); } return true; }, // Apply CSS custom properties for current mode applyCSS() { const colors = this.modes[this.currentMode].colors; const root = document.documentElement; Object.entries(colors).forEach(([name, value]) => { root.style.setProperty(`--cb-${name}`, value); }); // Also set filter for canvas elements if high contrast if (this.currentMode === 'highContrast') { root.style.setProperty('--cb-contrast-filter', 'contrast(1.2) saturate(1.3)'); } else { root.style.setProperty('--cb-contrast-filter', 'none'); } // v8.31: Apply colorblind pattern indicators this.applyPatternIndicators(); }, // v8.31: Add pattern-based indicators for colorblind users applyPatternIndicators() { let styleEl = document.getElementById('colorblind-patterns'); if (!styleEl) { styleEl = document.createElement('style'); styleEl.id = 'colorblind-patterns'; document.head.appendChild(styleEl); } // Only add patterns for non-normal modes if (this.currentMode === 'normal') { styleEl.textContent = ''; return; } styleEl.textContent = ` /* v8.31: Colorblind-friendly pattern indicators */ /* These patterns supplement color with shape recognition */ /* Health bar: horizontal stripes for low health warning */ .player-health-fill[style*="width: 2"], .player-health-fill[style*="width: 1"] { background-image: repeating-linear-gradient( 90deg, transparent 0px, transparent 8px, rgba(255,255,255,0.15) 8px, rgba(255,255,255,0.15) 10px ) !important; } /* Enemy indicators: diagonal stripes */ .enemy-indicator, [data-faction="enemy"] { background-image: repeating-linear-gradient( 45deg, transparent 0px, transparent 4px, rgba(0,0,0,0.2) 4px, rgba(0,0,0,0.2) 6px ); } /* Friendly indicators: dots pattern */ .friendly-indicator, [data-faction="friendly"] { background-image: radial-gradient( circle at 50% 50%, rgba(255,255,255,0.2) 2px, transparent 2px ); background-size: 8px 8px; } /* Buff indicators: upward chevrons */ .buff-indicator, [data-effect="buff"] { position: relative; } .buff-indicator::after, [data-effect="buff"]::after { content: ''; position: absolute; top: 2px; right: 2px; width: 6px; height: 6px; border-left: 2px solid rgba(255,255,255,0.5); border-top: 2px solid rgba(255,255,255,0.5); transform: rotate(45deg); } /* Debuff indicators: downward chevrons */ .debuff-indicator, [data-effect="debuff"] { position: relative; } .debuff-indicator::after, [data-effect="debuff"]::after { content: ''; position: absolute; top: 2px; right: 2px; width: 6px; height: 6px; border-right: 2px solid rgba(255,255,255,0.5); border-bottom: 2px solid rgba(255,255,255,0.5); transform: rotate(45deg); } /* Item rarity: border patterns */ .item-rare { border-style: dashed !important; } .item-epic { border-style: dotted !important; border-width: 3px !important; } .item-legendary { border-style: double !important; border-width: 4px !important; } .item-mythic { border-style: solid !important; box-shadow: inset 0 0 0 2px rgba(255,255,255,0.3) !important; } /* Ability ready vs cooldown: brightness change */ .ability-slot.on-cooldown { filter: brightness(0.5) grayscale(0.3); } `; }, // Cycle through modes cycleMode() { const modeNames = Object.keys(this.modes); const currentIndex = modeNames.indexOf(this.currentMode); const nextIndex = (currentIndex + 1) % modeNames.length; this.setMode(modeNames[nextIndex]); return this.currentMode; }, // Get list of available modes getModes() { return Object.entries(this.modes).map(([key, mode]) => ({ id: key, name: mode.name, description: mode.description, active: key === this.currentMode })); }, // Create settings panel HTML createSettingsPanel() { const container = document.createElement('div'); container.id = 'colorblind-settings'; container.innerHTML = `

Color Vision Settings

`; const modesContainer = container.querySelector('#cb-modes-container'); this.getModes().forEach(mode => { const btn = document.createElement('button'); btn.className = 'cb-mode-option' + (mode.active ? ' active' : ''); btn.innerHTML = `
${mode.name}
${mode.description}
${['damage', 'heal', 'rare', 'legendary', 'enemy'].map(c => `
` ).join('')}
`; btn.onclick = () => { this.setMode(mode.id); modesContainer.querySelectorAll('.cb-mode-option').forEach(b => b.classList.remove('active')); btn.classList.add('active'); }; modesContainer.appendChild(btn); }); container.querySelector('#colorblind-close').onclick = () => container.remove(); return container; }, // Show settings panel showSettings() { const existing = document.getElementById('colorblind-settings'); if (existing) { existing.remove(); return; } document.body.appendChild(this.createSettingsPanel()); if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('open'); } }, // Initialize from saved preferences init() { try { const saved = localStorage.getItem('leviathan_colorblind_mode'); if (saved && this.modes[saved]) { this.currentMode = saved; } } catch (e) {} this.applyCSS(); debugLog('ColorBlindnessSupport', 'Initialized - Mode:', this.modes[this.currentMode].name); // v8.25: gated } }; // Initialize color blindness support ColorBlindnessSupport.init(); // ============================================ // v7.5: Experimental Recipe Discovery System (8-Strategy Consensus Cycle 3) // Hidden recipes players can discover through material experimentation // Adds crafting depth and discovery-driven engagement // ============================================ const ExperimentalRecipes = { // Hidden recipes not shown until discovered hiddenRecipes: { 'void_crystal': { name: 'Void Crystal', ingredients: ['dark_matter', 'quantum_shard', 'stellar_dust'], description: 'A crystal pulsing with void energy', rarity: 'legendary', discovered: false, hint: 'Combine the darkness of space with quantum instability...', stats: { voidDamage: 50, energyRegen: 15 } }, 'phoenix_essence': { name: 'Phoenix Essence', ingredients: ['fire_core', 'plasma_cell', 'stellar_dust'], description: 'Burns eternally with rebirth energy', rarity: 'epic', discovered: false, hint: 'Fire reborn through stellar transformation...', stats: { fireDamage: 40, healthRegen: 10, reviveChance: 5 } }, 'quantum_stabilizer': { name: 'Quantum Stabilizer', ingredients: ['quantum_shard', 'quantum_shard', 'tech_component'], description: 'Locks quantum states in useful configurations', rarity: 'rare', discovered: false, hint: 'Double the quantum, stabilize with technology...', stats: { critChance: 15, dodgeChance: 10 } }, 'chrono_catalyst': { name: 'Chrono Catalyst', ingredients: ['quantum_shard', 'dark_matter', 'bio_sample'], description: 'Manipulates local time flow', rarity: 'legendary', discovered: false, hint: 'Time bends when quantum meets the void of life...', stats: { attackSpeed: 30, cooldownReduction: 20 } }, 'nebula_heart': { name: 'Nebula Heart', ingredients: ['stellar_dust', 'stellar_dust', 'plasma_cell'], description: 'Contains the essence of a dying star', rarity: 'epic', discovered: false, hint: 'Double stardust, ignited by plasma...', stats: { allDamage: 25, maxHealth: 100 } }, 'bio_fusion_core': { name: 'Bio-Fusion Core', ingredients: ['bio_sample', 'plasma_cell', 'tech_component'], description: 'Living technology that adapts and evolves', rarity: 'rare', discovered: false, hint: 'Life, energy, and technology merged...', stats: { healthRegen: 20, damageReflect: 10 } }, 'gravity_lens': { name: 'Gravity Lens', ingredients: ['dark_matter', 'dark_matter', 'quantum_shard'], description: 'Bends spacetime around the wielder', rarity: 'legendary', discovered: false, hint: 'Double the darkness, focused through uncertainty...', stats: { pullRadius: 50, damageAura: 15 } }, 'elemental_prism': { name: 'Elemental Prism', ingredients: ['fire_core', 'ice_shard', 'lightning_rod'], description: 'Refracts all elemental damage', rarity: 'epic', discovered: false, hint: 'The three elements in perfect balance...', stats: { fireDamage: 20, iceDamage: 20, lightningDamage: 20 } }, 'null_field_generator': { name: 'Null Field Generator', ingredients: ['dark_matter', 'tech_component', 'quantum_shard'], description: 'Creates zones of dampened reality', rarity: 'legendary', discovered: false, hint: 'Technology to contain the uncontainable...', stats: { damageReduction: 30, statusImmunity: true } }, 'synthesis_matrix': { name: 'Synthesis Matrix', ingredients: ['tech_component', 'tech_component', 'bio_sample'], description: 'Enhances all crafting outcomes', rarity: 'rare', discovered: false, hint: 'Technology duplicated, infused with life...', stats: { craftingBonus: 25, resourceYield: 15 } } }, // Track discovery progress discoveredRecipes: new Set(), experimentHistory: [], maxHistorySize: 50, // Attempt to combine ingredients experiment(ingredientIds) { if (!Array.isArray(ingredientIds) || ingredientIds.length !== 3) { return { success: false, message: 'Experiments require exactly 3 ingredients' }; } // Sort for consistent matching const sorted = [...ingredientIds].sort(); const key = sorted.join('_'); // Record experiment in history this.experimentHistory.unshift({ ingredients: sorted, timestamp: Date.now() }); if (this.experimentHistory.length > this.maxHistorySize) { this.experimentHistory.pop(); } // Check against hidden recipes for (const [recipeId, recipe] of Object.entries(this.hiddenRecipes)) { const recipeSorted = [...recipe.ingredients].sort(); if (recipeSorted.join('_') === key) { // Discovery! if (!this.discoveredRecipes.has(recipeId)) { this.discoveredRecipes.add(recipeId); recipe.discovered = true; this.saveProgress(); // Play discovery sound if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('discover'); } // Haptic feedback if (typeof MobileHaptics !== 'undefined') { MobileHaptics.vibrate('levelUp'); } return { success: true, newDiscovery: true, recipe: recipe, message: `NEW DISCOVERY: ${recipe.name}!` }; } else { // Already known if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('craft'); } return { success: true, newDiscovery: false, recipe: recipe, message: `Crafted: ${recipe.name}` }; } } } // Failed experiment - but give hints based on partial matches const hint = this.getPartialMatchHint(sorted); if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('error'); } return { success: false, hint: hint, message: hint || 'The materials react but produce nothing useful...' }; }, // Get hint based on partial matches getPartialMatchHint(ingredients) { let bestMatch = 0; let bestHint = null; for (const [recipeId, recipe] of Object.entries(this.hiddenRecipes)) { if (recipe.discovered) continue; const recipeSorted = [...recipe.ingredients].sort(); let matches = 0; for (const ing of ingredients) { if (recipeSorted.includes(ing)) matches++; } if (matches > bestMatch) { bestMatch = matches; if (matches >= 2) { bestHint = `You sense potential... ${recipe.hint}`; } else if (matches === 1) { bestHint = 'One ingredient resonates with hidden knowledge...'; } } } return bestHint; }, // Get all discovered recipes getDiscovered() { return Object.entries(this.hiddenRecipes) .filter(([_, r]) => r.discovered) .map(([id, r]) => ({ id, ...r })); }, // Get discovery progress getProgress() { const total = Object.keys(this.hiddenRecipes).length; const discovered = this.discoveredRecipes.size; return { discovered, total, percentage: Math.round((discovered / total) * 100), remaining: total - discovered }; }, // Get hints for undiscovered recipes (vague) getHints() { return Object.entries(this.hiddenRecipes) .filter(([_, r]) => !r.discovered) .map(([id, r]) => ({ rarity: r.rarity, hint: r.hint })); }, // Save progress to localStorage saveProgress() { try { localStorage.setItem('leviathan_experiments', JSON.stringify({ discovered: Array.from(this.discoveredRecipes), history: this.experimentHistory.slice(0, 20) // Save last 20 experiments })); } catch (e) { debugWarn('ExperimentalRecipes', 'Failed to save:', e); // v8.25: gated } }, // Load progress from localStorage // v8.28: Use ErrorRecovery.safeJSONParse for safer parsing loadProgress() { try { const stored = ErrorRecovery.safeLocalStorage.get('leviathan_experiments'); if (stored) { const data = ErrorRecovery.safeJSONParse(stored, null); if (data) { this.discoveredRecipes = new Set(data.discovered || []); this.experimentHistory = data.history || []; // Mark discovered recipes for (const recipeId of this.discoveredRecipes) { if (this.hiddenRecipes[recipeId]) { this.hiddenRecipes[recipeId].discovered = true; } } } } } catch (e) { debugWarn('ExperimentalRecipes', 'Failed to load progress:', e); // v8.25: gated } }, // Create experimentation UI panel createPanel() { const panel = document.createElement('div'); panel.id = 'experiment-panel'; panel.innerHTML = `

⚗️ Experimental Synthesis

?
?
?
Select 3 materials to experiment with unknown combinations...

Discovered Recipes:

`; const progress = this.getProgress(); panel.querySelector('.progress-text').textContent = `${progress.discovered}/${progress.total} recipes discovered (${progress.percentage}%)`; panel.querySelector('.exp-progress-fill').style.width = `${progress.percentage}%`; // Populate discovered recipes const discoveredList = panel.querySelector('.discovered-list'); const discovered = this.getDiscovered(); if (discovered.length > 0) { discovered.forEach(recipe => { const tag = document.createElement('span'); tag.className = `exp-recipe-tag ${recipe.rarity}`; tag.textContent = recipe.name; tag.title = recipe.description; discoveredList.appendChild(tag); }); } else { discoveredList.innerHTML = 'No recipes discovered yet...'; } panel.querySelector('#experiment-close').onclick = () => panel.remove(); return panel; }, // Show experimentation panel showPanel() { const existing = document.getElementById('experiment-panel'); if (existing) { existing.remove(); return; } document.body.appendChild(this.createPanel()); if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('open'); } }, // Initialize init() { this.loadProgress(); const progress = this.getProgress(); debugLog('ExperimentalRecipes', 'Initialized -', progress.discovered, '/', progress.total, 'discovered'); // v8.25: gated } }; // Initialize experimental recipes system ExperimentalRecipes.init(); // ============================================ // v7.5: Boss Phase Transition System (8-Strategy Consensus Cycle 4) // Dynamic boss fights with HP-threshold behavior changes // Creates escalating tension and memorable encounters // ============================================ const BossPhaseSystem = { // Phase configurations for each boss type phases: { 'Terra_Boss': [ { threshold: 0.7, speedMult: 1.2, attackWindupMult: 0.9, damageMult: 1.0, message: 'The Guardian stirs!', color: 0x44aa44 }, { threshold: 0.4, speedMult: 1.5, attackWindupMult: 0.75, damageMult: 1.3, message: 'Ancient rage awakens!', color: 0xaaaa44 }, { threshold: 0.2, speedMult: 2.0, attackWindupMult: 0.5, damageMult: 1.6, enraged: true, message: 'TERRA UNLEASHED!', color: 0xff4444 } ], 'Desert_Boss': [ { threshold: 0.7, speedMult: 1.15, attackWindupMult: 0.95, damageMult: 1.0, message: 'The Sandstorm rises!', color: 0xddaa44 }, { threshold: 0.4, speedMult: 1.4, attackWindupMult: 0.8, damageMult: 1.25, message: 'Desert fury intensifies!', color: 0xff8844 }, { threshold: 0.2, speedMult: 1.8, attackWindupMult: 0.6, damageMult: 1.5, enraged: true, message: 'SANDSTORM SUPREME!', color: 0xff4400 } ], 'Ice_Boss': [ { threshold: 0.7, speedMult: 1.1, attackWindupMult: 0.9, damageMult: 1.1, message: 'Frost aura intensifies!', color: 0x44aaff }, { threshold: 0.4, speedMult: 1.3, attackWindupMult: 0.7, damageMult: 1.35, message: 'Blizzard approaching!', color: 0x88ccff }, { threshold: 0.2, speedMult: 1.6, attackWindupMult: 0.55, damageMult: 1.6, enraged: true, message: 'ABSOLUTE ZERO!', color: 0xffffff } ], 'Volcanic_Boss': [ { threshold: 0.7, speedMult: 1.25, attackWindupMult: 0.85, damageMult: 1.15, message: 'Magma core heating!', color: 0xff6600 }, { threshold: 0.4, speedMult: 1.5, attackWindupMult: 0.65, damageMult: 1.4, message: 'Volcanic eruption imminent!', color: 0xff4400 }, { threshold: 0.2, speedMult: 1.9, attackWindupMult: 0.45, damageMult: 1.7, enraged: true, message: 'MELTDOWN PROTOCOL!', color: 0xff0000 } ], 'Alien_Boss': [ { threshold: 0.7, speedMult: 1.3, attackWindupMult: 0.8, damageMult: 1.1, message: 'Xenoform adapting!', color: 0xaa44ff }, { threshold: 0.4, speedMult: 1.6, attackWindupMult: 0.6, damageMult: 1.45, message: 'Reality destabilizing!', color: 0xff44aa }, { threshold: 0.2, speedMult: 2.2, attackWindupMult: 0.4, damageMult: 1.8, enraged: true, message: 'COSMIC ANNIHILATION!', color: 0xff00ff } ], // Default phases for any boss without specific config 'default': [ { threshold: 0.6, speedMult: 1.2, attackWindupMult: 0.85, damageMult: 1.1, message: 'The boss grows stronger!', color: 0xffaa00 }, { threshold: 0.3, speedMult: 1.5, attackWindupMult: 0.65, damageMult: 1.4, enraged: true, message: 'ENRAGED!', color: 0xff4444 } ] }, // Check and apply phase transitions for a boss checkPhaseTransition(boss) { if (!boss || !boss.userData || !boss.userData.isBoss) return null; const bossId = boss.userData.bossId || 'default'; const phases = this.phases[bossId] || this.phases['default']; const currentPhase = boss.userData.currentPhase || 0; if (currentPhase >= phases.length) return null; // All phases triggered const hpPercent = boss.userData.hp / boss.userData.maxHp; const nextPhase = phases[currentPhase]; if (hpPercent <= nextPhase.threshold) { // Trigger phase transition! this.applyPhase(boss, nextPhase, currentPhase + 1); return nextPhase; } return null; }, // Apply phase modifiers to boss applyPhase(boss, phase, phaseNumber) { boss.userData.currentPhase = phaseNumber; // Apply stat modifiers boss.userData.speedMultiplier = phase.speedMult || 1; boss.userData.damageMultiplier = phase.damageMult || 1; boss.userData.attackWindupMultiplier = phase.attackWindupMult || 1; boss.userData.enraged = phase.enraged || false; // Visual feedback - change emissive color // v10.12: Added emissive property check to avoid errors on MeshBasicMaterial if (boss.material && boss.material.emissive && phase.color) { const originalEmissive = boss.material.emissive.getHex(); boss.userData.originalEmissive = boss.userData.originalEmissive || originalEmissive; // Pulse to new color boss.material.emissive.setHex(phase.color); boss.material.emissiveIntensity = 2.0; // Gradual settle setTimeout(() => { if (boss.material && boss.material.emissive) boss.material.emissiveIntensity = phase.enraged ? 1.5 : 1.0; }, 500); } // Scale pulse effect if (boss.scale) { const originalScale = boss.userData.originalScale || boss.scale.x; boss.userData.originalScale = originalScale; boss.scale.setScalar(originalScale * 1.3); setTimeout(() => { if (boss.scale) boss.scale.setScalar(originalScale * (phase.enraged ? 1.1 : 1.0)); }, 300); } // Show phase message if (phase.message) { if (typeof showNotification === 'function') { showNotification(phase.message, phase.enraged ? 'error' : 'warning'); } // Spawn dramatic floater if (boss.position && typeof spawnFloater === 'function') { const floaterPos = boss.position.clone(); floaterPos.y += 3; spawnFloater(floaterPos, phase.message, phase.enraged ? '#ff4444' : '#ffaa00'); } } // Screen shake for phase transition if (typeof screenShake === 'function') { screenShake(phase.enraged ? 0.8 : 0.4); } // Audio feedback if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play(phase.enraged ? 'warning' : 'notification'); } // Haptic feedback if (typeof MobileHaptics !== 'undefined') { MobileHaptics.vibrate(phase.enraged ? 'death' : 'levelUp'); } // Particle burst if (typeof particles !== 'undefined' && particles.emit && boss.position) { particles.emit(boss.position, phase.enraged ? 60 : 30, phase.color, { spread: 8, lifetime: 1500, gravity: -2 }); } debugLog('BossPhaseSystem', `${boss.userData.bossId || 'Boss'} entered phase ${phaseNumber}: ${phase.message}`); // v8.25: gated }, // Reset boss phases (called when boss spawns) resetBoss(boss) { if (boss && boss.userData) { boss.userData.currentPhase = 0; boss.userData.speedMultiplier = 1; boss.userData.damageMultiplier = 1; boss.userData.attackWindupMultiplier = 1; boss.userData.enraged = false; } }, // Get phase progress bar data for UI getPhaseInfo(boss) { if (!boss || !boss.userData || !boss.userData.isBoss) return null; const bossId = boss.userData.bossId || 'default'; const phases = this.phases[bossId] || this.phases['default']; const currentPhase = boss.userData.currentPhase || 0; const hpPercent = boss.userData.hp / boss.userData.maxHp; return { currentPhase, totalPhases: phases.length, hpPercent, nextThreshold: currentPhase < phases.length ? phases[currentPhase].threshold : 0, isEnraged: boss.userData.enraged || false }; } }; debugLog('BossPhaseSystem', 'Initialized with', Object.keys(BossPhaseSystem.phases).length - 1, 'boss configurations'); // v8.25: gated // ============================================ // v7.5: Session Summary & Wellness System (8-Strategy Consensus Cycle 4) // Tracks session progress and provides wellness break reminders // Enhances player retention through accomplishment visibility // ============================================ const SessionWellness = { sessionStartTime: Date.now(), lastReminderTime: Date.now(), breakReminderEnabled: true, breakReminderInterval: 30, // minutes // Capture metrics at session start startMetrics: null, // Initialize and capture starting metrics init() { this.sessionStartTime = Date.now(); this.lastReminderTime = Date.now(); // v8.28: Use ErrorRecovery for safer localStorage and JSON parsing const stored = ErrorRecovery.safeLocalStorage.get('leviathan_wellness'); if (stored) { const prefs = ErrorRecovery.safeJSONParse(stored, null); if (prefs) { this.breakReminderEnabled = prefs.enabled !== false; this.breakReminderInterval = prefs.interval || 30; } } // Capture starting metrics this.captureStartMetrics(); debugLog('SessionWellness', 'Initialized - Reminders:', this.breakReminderEnabled ? `every ${this.breakReminderInterval}min` : 'disabled'); // v8.25: gated }, // Capture metrics at session start for comparison // Deferred until gameData is available to avoid TDZ errors captureStartMetrics() { this.startMetrics = { timestamp: Date.now(), playtime: 0, totalXP: 0, mobsKilled: 0, bossesDefeated: 0, itemsCrafted: 0, planetsVisited: 0, resourcesGathered: 0 }; // Will be updated when gameData is ready }, // Called after gameData is initialized to capture actual metrics captureStartMetricsDeferred() { try { this.startMetrics = { timestamp: this.startMetrics?.timestamp || Date.now(), playtime: gameData?.playtime || 0, totalXP: this.getTotalXP(), mobsKilled: this.getStat('mobsKilled'), bossesDefeated: this.getStat('bossesDefeated'), itemsCrafted: this.getStat('itemsCrafted'), planetsVisited: this.getStat('planetsVisited'), resourcesGathered: this.getStat('resourcesGathered') }; } catch (e) { /* gameData not ready yet */ } }, // Helper to get total XP across all skills getTotalXP() { try { if (typeof gameData === 'undefined' || !gameData?.skills) return 0; return Object.values(gameData.skills).reduce((sum, skill) => sum + (skill.xp || 0), 0); } catch (e) { return 0; } }, // Helper to get a statistic safely getStat(statName) { try { if (typeof gameData === 'undefined' || !gameData?.statistics) return 0; return gameData.statistics[statName] || 0; } catch (e) { return 0; } }, // Get current session duration in minutes getSessionMinutes() { return Math.floor((Date.now() - this.sessionStartTime) / 60000); }, // Check if break reminder should show checkBreakReminder() { if (!this.breakReminderEnabled) return; const timeSinceReminder = Date.now() - this.lastReminderTime; const intervalMs = this.breakReminderInterval * 60 * 1000; if (timeSinceReminder >= intervalMs) { this.showBreakReminder(); this.lastReminderTime = Date.now(); } }, // Show gentle break reminder showBreakReminder() { const sessionMins = this.getSessionMinutes(); const messages = [ `You've been exploring for ${sessionMins} minutes. Time for a stretch?`, `${sessionMins} minutes of adventure! Remember to rest your eyes.`, `Great progress! ${sessionMins} minutes played. Consider a quick break!`, `The cosmos will wait! ${sessionMins}min session - hydration check?` ]; const message = messages[Math.floor(Math.random() * messages.length)]; if (typeof showNotification === 'function') { showNotification(message, 'info'); } if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('notification'); } }, // Get session summary data getSessionSummary() { if (!this.startMetrics) return null; const sessionMins = this.getSessionMinutes(); const xpGained = this.getTotalXP() - this.startMetrics.totalXP; const mobsKilled = this.getStat('mobsKilled') - this.startMetrics.mobsKilled; const bossesDefeated = this.getStat('bossesDefeated') - this.startMetrics.bossesDefeated; const itemsCrafted = this.getStat('itemsCrafted') - this.startMetrics.itemsCrafted; const planetsVisited = this.getStat('planetsVisited') - this.startMetrics.planetsVisited; const resourcesGathered = this.getStat('resourcesGathered') - this.startMetrics.resourcesGathered; return { duration: sessionMins, xpGained, mobsKilled, bossesDefeated, itemsCrafted, planetsVisited, resourcesGathered, hasProgress: xpGained > 0 || mobsKilled > 0 || itemsCrafted > 0 }; }, // Show session summary (called on tab hide/close) showSessionSummary() { const summary = this.getSessionSummary(); if (!summary || !summary.hasProgress || summary.duration < 1) return; // Build summary message const parts = []; if (summary.duration > 0) parts.push(`${summary.duration}min played`); if (summary.xpGained > 0) parts.push(`+${summary.xpGained.toLocaleString()} XP`); if (summary.mobsKilled > 0) parts.push(`${summary.mobsKilled} defeated`); if (summary.bossesDefeated > 0) parts.push(`${summary.bossesDefeated} bosses`); if (parts.length === 0) return; const message = 'Session: ' + parts.join(' • '); if (typeof showNotification === 'function') { showNotification(message, 'success'); } }, // Create settings panel createSettingsPanel() { const panel = document.createElement('div'); panel.id = 'wellness-settings'; panel.innerHTML = `

🧘 Wellness Settings

Break Reminders
Gentle reminders to take breaks
Reminder Interval
Time between reminders
${this.breakReminderInterval}min
This Session
Duration${this.getSessionMinutes()}min
XP Gained-
Enemies Defeated-
`; // Update session stats const summary = this.getSessionSummary(); if (summary) { panel.querySelector('#wellness-xp').textContent = summary.xpGained.toLocaleString(); panel.querySelector('#wellness-kills').textContent = summary.mobsKilled.toLocaleString(); } // Event handlers panel.querySelector('#wellness-close').onclick = () => panel.remove(); panel.querySelector('#wellness-toggle-reminder').onclick = (e) => { this.breakReminderEnabled = !this.breakReminderEnabled; e.target.classList.toggle('active', this.breakReminderEnabled); this.savePreferences(); }; panel.querySelector('#wellness-interval').oninput = (e) => { this.breakReminderInterval = parseInt(e.target.value); panel.querySelector('#wellness-interval-value').textContent = `${this.breakReminderInterval}min`; this.savePreferences(); }; return panel; }, // Show settings panel showSettings() { const existing = document.getElementById('wellness-settings'); if (existing) { existing.remove(); return; } document.body.appendChild(this.createSettingsPanel()); if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('open'); } }, // Save preferences savePreferences() { try { localStorage.setItem('leviathan_wellness', JSON.stringify({ enabled: this.breakReminderEnabled, interval: this.breakReminderInterval })); } catch (e) {} } }; // Initialize wellness system SessionWellness.init(); // v8.39: Hook into visibility change for session summary via centralized manager PageVisibilityManager.subscribe('sessionWellness', (isVisible) => { if (!isVisible) { SessionWellness.showSessionSummary(); } }); // ============================================ // v7.5: Unified Modal Transitions System (8-Strategy Consensus Cycle 4) // Consistent open/close animations across all modal interfaces // Includes unified close button styling and accessibility support // ============================================ const ModalTransitions = { // Inject unified modal CSS init() { const style = document.createElement('style'); style.id = 'unified-modal-styles'; style.textContent = ` /* === UNIFIED CLOSE BUTTON === */ .close-btn-unified { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.1); border: none; border-radius: 50%; color: rgba(255, 255, 255, 0.7); font-size: 20px; cursor: pointer; transition: all 0.2s ease; line-height: 1; position: absolute; top: 12px; right: 12px; } .close-btn-unified:hover { background: rgba(255, 68, 68, 0.3); color: #ff6666; transform: rotate(90deg); } .close-btn-unified:active { transform: rotate(90deg) scale(0.9); } .close-btn-unified:focus-visible { outline: 2px solid #44aaff; outline-offset: 2px; } /* === UNIFIED MODAL OVERLAY === */ .modal-unified { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 10000; opacity: 0; visibility: hidden; transition: opacity 0.25s ease, visibility 0s 0.25s; } .modal-unified.active { opacity: 1; visibility: visible; transition: opacity 0.25s ease, visibility 0s; } /* === UNIFIED MODAL CONTENT === */ .modal-content-unified { position: relative; background: linear-gradient(135deg, rgba(20, 25, 40, 0.98), rgba(30, 35, 50, 0.98)); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 12px; padding: 24px; max-width: 90vw; max-height: 85vh; overflow-y: auto; transform: scale(0.95) translateY(10px); transition: transform 0.25s ease; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5); } .modal-unified.active .modal-content-unified { transform: scale(1) translateY(0); } /* === MODAL HEADER === */ .modal-header-unified { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .modal-title-unified { font-size: 18px; font-weight: 600; color: #ffffff; margin: 0; } /* === BUTTON DISABLED STATES === */ button:disabled, button.disabled, .btn:disabled, .btn.disabled, [role="button"][aria-disabled="true"] { opacity: 0.4 !important; cursor: not-allowed !important; pointer-events: none !important; filter: grayscale(30%); transition: opacity var(--transition-base), filter var(--transition-base), box-shadow var(--transition-base); /* v7.54: Smooth disabled state animations (Cycle 33 Visual Polish) */ transform: none !important; box-shadow: none !important; } /* === MODAL FOOTER === */ .modal-footer-unified { display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px; padding-top: 16px; border-top: 1px solid rgba(255, 255, 255, 0.1); } /* === REDUCED MOTION SUPPORT === */ @media (prefers-reduced-motion: reduce) { .modal-unified, .modal-content-unified, .close-btn-unified { transition: none !important; } .close-btn-unified:hover { transform: none !important; } } /* === MODAL SCROLLBAR === */ .modal-content-unified::-webkit-scrollbar { width: 6px; } .modal-content-unified::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); border-radius: 3px; } .modal-content-unified::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 3px; } .modal-content-unified::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); } `; document.head.appendChild(style); debugLog('ModalTransitions', 'Unified styles injected'); // v8.25: gated }, // Create a unified modal create(options = {}) { const { id = 'modal-' + Date.now(), title = '', content = '', width = '400px', onClose = null, showCloseButton = true } = options; const overlay = document.createElement('div'); overlay.id = id; overlay.className = 'modal-unified'; overlay.innerHTML = ` `; // Close button handler if (showCloseButton) { overlay.querySelector('.close-btn-unified').onclick = () => { this.close(id); if (onClose) onClose(); }; } // Click outside to close overlay.addEventListener('click', (e) => { if (e.target === overlay) { this.close(id); if (onClose) onClose(); } }); // Escape key to close const escHandler = (e) => { if (e.key === 'Escape' && document.getElementById(id)) { this.close(id); if (onClose) onClose(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); document.body.appendChild(overlay); // Trigger opening animation requestAnimationFrame(() => { overlay.classList.add('active'); }); // Play sound if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('open'); } return overlay; }, // Close a modal by ID close(id) { const modal = document.getElementById(id); if (!modal) return; modal.classList.remove('active'); // Play close sound if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('close'); } // Remove after transition setTimeout(() => { if (modal.parentNode) { modal.parentNode.removeChild(modal); } }, 250); }, // Show a simple alert modal alert(message, title = 'Notice') { return this.create({ title, content: `

${message}

`, width: '350px' }); }, // Show a confirmation modal confirm(message, title = 'Confirm', onYes, onNo) { const modal = this.create({ title, content: `

${message}

`, width: '380px', showCloseButton: false }); modal.querySelector('#modal-confirm-yes').onclick = () => { this.close(modal.id); if (onYes) onYes(); }; modal.querySelector('#modal-confirm-no').onclick = () => { this.close(modal.id); if (onNo) onNo(); }; return modal; } }; // Initialize modal transition system ModalTransitions.init(); // ============================================ // v7.5: Global Error Boundary with Auto-Recovery (8-Strategy Consensus Cycle 5) // Catches uncaught exceptions, performs auto-save, notifies users gracefully // Prevents error cascades and maintains game stability // ============================================ const GlobalErrorBoundary = { errorCount: 0, maxErrors: 10, errorThrottleMs: 1000, lastErrorTime: 0, autoSaveOnError: true, hasShownFatalNotice: false, errorLog: [], maxLogSize: 50, init() { // Global error handler for synchronous errors window.onerror = (message, source, lineno, colno, error) => { this.handleError({ type: 'error', message: message, source: source, line: lineno, column: colno, error: error }); return true; // Prevent default browser error handling }; // Handler for unhandled promise rejections window.onunhandledrejection = (event) => { this.handleError({ type: 'unhandledrejection', message: event.reason?.message || String(event.reason), error: event.reason }); event.preventDefault(); }; debugLog('GlobalErrorBoundary', 'Initialized - protecting game stability'); // v8.25: gated }, handleError(errorInfo) { const now = Date.now(); // Throttle rapid errors if (now - this.lastErrorTime < this.errorThrottleMs) { return; } this.lastErrorTime = now; this.errorCount++; // Log the error this.logError(errorInfo); // Auto-save on first few errors if (this.errorCount <= 3 && this.autoSaveOnError) { this.performAutoSave(); } // Show user notification for non-critical errors if (this.errorCount <= this.maxErrors) { this.showErrorNotification(errorInfo); } // Fatal error threshold reached if (this.errorCount >= this.maxErrors && !this.hasShownFatalNotice) { this.hasShownFatalNotice = true; this.showFatalErrorNotice(); } // Console log for debugging console.error('[GlobalErrorBoundary] Caught error:', errorInfo.message); }, logError(errorInfo) { const logEntry = { timestamp: new Date().toISOString(), type: errorInfo.type, message: errorInfo.message, source: errorInfo.source || 'unknown', line: errorInfo.line || 0, stack: errorInfo.error?.stack || null }; this.errorLog.push(logEntry); // Keep log size manageable if (this.errorLog.length > this.maxLogSize) { this.errorLog.shift(); } // Persist error log try { localStorage.setItem('leviathan_error_log', JSON.stringify(this.errorLog.slice(-20))); } catch (e) {} }, performAutoSave() { try { // Trigger game auto-save if available if (typeof saveGame === 'function') { saveGame(); debugLog('GlobalErrorBoundary', 'Auto-save performed after error'); // v8.25: gated } else if (typeof gameState !== 'undefined') { localStorage.setItem('leviathan_emergency_save', JSON.stringify(gameState)); debugLog('GlobalErrorBoundary', 'Emergency save performed'); // v8.25: gated } } catch (e) { debugWarn('GlobalErrorBoundary', 'Auto-save failed:', e); // v8.25: gated } }, showErrorNotification(errorInfo) { // Don't spam notifications if (document.querySelector('.error-boundary-notification')) return; const notification = document.createElement('div'); notification.className = 'error-boundary-notification'; notification.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: linear-gradient(135deg, rgba(80, 30, 30, 0.95), rgba(60, 20, 20, 0.95)); border: 1px solid rgba(255, 100, 100, 0.3); border-radius: 8px; padding: 12px 16px; max-width: 320px; z-index: 100000; font-family: system-ui, -apple-system, sans-serif; animation: errorSlideIn 0.3s ease; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); `; notification.innerHTML = `
⚠️
Minor Issue Detected
The game encountered a small problem but is still running. Your progress has been auto-saved.
`; document.body.appendChild(notification); // Play error sound if available if (typeof UISoundSystem !== 'undefined') { UISoundSystem.play('error'); } // Auto-dismiss after 5 seconds setTimeout(() => { if (notification.parentNode) { notification.style.animation = 'errorSlideOut 0.3s ease forwards'; setTimeout(() => notification.remove(), 300); } }, 5000); }, showFatalErrorNotice() { // Use ModalTransitions if available, otherwise create standalone if (typeof ModalTransitions !== 'undefined') { ModalTransitions.create({ id: 'fatal-error-modal', title: '⚠️ Multiple Errors Detected', content: `

The game has encountered multiple errors. This might affect gameplay.

Your progress has been auto-saved. You can continue playing or refresh the page.

`, width: '420px', showCloseButton: true }); } else { // Fallback modal alert('The game encountered multiple errors. Your progress has been saved. Please refresh the page.'); } }, // Reset error count (useful after successful recovery) reset() { this.errorCount = 0; this.hasShownFatalNotice = false; debugLog('GlobalErrorBoundary', 'Error count reset'); // v8.25: gated }, // Get error statistics getStats() { return { errorCount: this.errorCount, maxErrors: this.maxErrors, recentErrors: this.errorLog.slice(-5), hasFatalNotice: this.hasShownFatalNotice }; } }; // Initialize global error boundary GlobalErrorBoundary.init(); // ============================================ // v7.5: Animated Loading Progress System (8-Strategy Consensus Cycle 5) // Provides smooth, animated progress updates during game initialization // Replaces static loading with dynamic visual feedback // ============================================ const AnimatedLoadingProgress = { currentProgress: 0, targetProgress: 0, animationFrame: null, // v8.28: Enhanced phase definitions with descriptive messages phases: [ { id: 1, name: 'Audio', start: 0, end: 15, message: 'Initializing audio systems...' }, { id: 2, name: 'Renderer', start: 15, end: 40, message: 'Setting up 3D renderer...' }, { id: 3, name: 'World', start: 40, end: 70, message: 'Generating galaxy...' }, { id: 4, name: 'Assets', start: 70, end: 100, message: 'Loading game assets...' } ], initialized: false, init() { if (this.initialized) return; this.initialized = true; // Inject enhanced loading styles const style = document.createElement('style'); style.id = 'animated-loading-styles'; style.textContent = ` /* === ENHANCED LOADING BAR === */ .loading-progress { height: 100%; background: linear-gradient(90deg, #0f0, #0ff, #0f0); background-size: 200% 100%; animation: loadShimmer 2s linear infinite, loadPulse 1s ease-in-out infinite; transition: width 0.3s ease-out; position: relative; } .loading-progress::after { content: ''; position: absolute; right: 0; top: 0; width: 20px; height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent); animation: loadGlow 0.8s ease-in-out infinite; } @keyframes loadShimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } @keyframes loadGlow { 0%, 100% { opacity: 0.5; } 50% { opacity: 1; } } /* === PROGRESS PERCENTAGE === */ .loading-percentage { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 12px; color: rgba(0, 255, 0, 0.8); font-family: monospace; text-shadow: 0 0 10px rgba(0, 255, 0, 0.5); pointer-events: none; z-index: 5; } /* === PHASE DOTS ANIMATION === */ #loading-phases span { transition: color 0.3s ease, transform 0.3s ease, text-shadow 0.3s ease; } #loading-phases span.active { color: #0f0 !important; transform: scale(1.1); text-shadow: 0 0 8px rgba(0, 255, 0, 0.6); } #loading-phases span.completed { color: #0a0 !important; } /* === LOADING TEXT ANIMATION === */ .loading-text { animation: textGlow 2s ease-in-out infinite; } @keyframes textGlow { 0%, 100% { text-shadow: 0 0 10px rgba(0, 255, 0, 0.3); } 50% { text-shadow: 0 0 20px rgba(0, 255, 0, 0.6), 0 0 30px rgba(0, 255, 255, 0.3); } } /* === LOADING TIP FADE === */ #loading-tip { animation: tipFade 4s ease-in-out infinite; } @keyframes tipFade { 0%, 100% { opacity: 0.6; } 50% { opacity: 1; } } `; document.head.appendChild(style); // Add percentage display to loading bar const loadingBar = document.querySelector('.loading-bar'); if (loadingBar && !document.querySelector('.loading-percentage')) { loadingBar.style.position = 'relative'; const percentage = document.createElement('div'); percentage.className = 'loading-percentage'; percentage.id = 'loading-percentage'; percentage.textContent = '0%'; loadingBar.appendChild(percentage); } debugLog('AnimatedLoadingProgress', 'Initialized'); // v8.25: gated }, // Set progress to a target value with animation setProgress(target, immediate = false) { this.targetProgress = Math.min(100, Math.max(0, target)); if (immediate) { this.currentProgress = this.targetProgress; this.updateDisplay(); return; } // Cancel any existing animation if (this.animationFrame) { cancelAnimationFrame(this.animationFrame); } this.animate(); }, // Animate progress bar smoothly // v8.28: Use Easing.easeOutCubic for smoother progress animation animate() { // v8.34: Skip animation when tab is hidden if (!isPageVisible) { this.animationFrame = requestAnimationFrame(() => this.animate()); return; } const diff = this.targetProgress - this.currentProgress; if (Math.abs(diff) < 0.5) { this.currentProgress = this.targetProgress; this.updateDisplay(); return; } // v8.28: Apply easeOutCubic for natural deceleration feel const easingFactor = typeof Easing !== 'undefined' ? 0.12 : 0.1; this.currentProgress += diff * easingFactor; this.updateDisplay(); this.animationFrame = requestAnimationFrame(() => this.animate()); }, // Update the visual display updateDisplay() { const progressBar = document.getElementById('loading-progress-bar'); const percentage = document.getElementById('loading-percentage'); const loadingBarWrapper = document.querySelector('.loading-bar'); if (progressBar) { progressBar.style.width = `${this.currentProgress}%`; // Remove the automatic animation, use actual width progressBar.style.marginLeft = '0'; } if (percentage) { percentage.textContent = `${Math.round(this.currentProgress)}%`; } if (loadingBarWrapper) { loadingBarWrapper.setAttribute('aria-valuenow', Math.round(this.currentProgress)); } // Update phase indicators this.updatePhaseIndicators(); }, // Update phase indicator dots updatePhaseIndicators() { this.phases.forEach(phase => { const el = document.getElementById('phase-' + phase.id); if (!el) return; if (this.currentProgress >= phase.end) { el.classList.remove('active'); el.classList.add('completed'); } else if (this.currentProgress >= phase.start) { el.classList.add('active'); el.classList.remove('completed'); } else { el.classList.remove('active', 'completed'); } }); }, // Helper to set phase directly // v8.28: Automatically uses phase.message if no message provided setPhase(phaseNumber, message = null) { const phase = this.phases.find(p => p.id === phaseNumber); if (!phase) return; // Set progress to phase start this.setProgress(phase.start); // v8.28: Use phase's built-in message if none provided const displayMessage = message || phase.message; if (displayMessage) { const phaseEl = document.getElementById('loading-phase'); if (phaseEl) phaseEl.textContent = displayMessage; } }, // Complete loading (100%) complete() { this.setProgress(100); }, // Reset for new load reset() { this.currentProgress = 0; this.targetProgress = 0; this.updateDisplay(); } }; // Initialize animated loading on script load AnimatedLoadingProgress.init(); // ============================================ // v7.5: Safe JSON Parser Utility (8-Strategy Consensus Cycle 5) // Centralizes JSON parsing with graceful error handling // Provides default values, validation, and repair capabilities // ============================================ const SafeJSON = { // Statistics for monitoring stats: { parseAttempts: 0, parseSuccesses: 0, parseFailures: 0, repairAttempts: 0, repairSuccesses: 0 }, /** * Safely parse JSON with default value fallback * @param {string} str - JSON string to parse * @param {*} defaultValue - Value to return on failure (default: null) * @param {Object} options - Options: { silent, repair, validate, log } * @returns {*} Parsed value or default */ parse(str, defaultValue = null, options = {}) { const { silent = false, repair = true, validate = null, log = false } = options; this.stats.parseAttempts++; // Handle null/undefined/empty if (str === null || str === undefined || str === '') { if (log) debugLog('SafeJSON', 'Empty input, returning default'); // v8.25: gated return defaultValue; } // Ensure string type if (typeof str !== 'string') { if (typeof str === 'object') { // Already an object, return as-is this.stats.parseSuccesses++; return str; } str = String(str); } try { const parsed = JSON.parse(str); // Validate if validator provided if (validate && typeof validate === 'function') { if (!validate(parsed)) { if (!silent) { debugWarn('SafeJSON', 'Validation failed'); // v8.25: gated } return defaultValue; } } this.stats.parseSuccesses++; return parsed; } catch (error) { this.stats.parseFailures++; // Attempt repair if enabled if (repair) { const repaired = this.attemptRepair(str); if (repaired !== null) { this.stats.repairSuccesses++; if (log) debugLog('SafeJSON', 'Successfully repaired JSON'); // v8.25: gated return repaired; } } if (!silent) { debugWarn('SafeJSON', 'Parse failed:', error.message); // v8.25: gated } return defaultValue; } }, /** * Safely stringify with error handling * @param {*} value - Value to stringify * @param {Object} options - Options: { pretty, replacer, defaultValue } * @returns {string|null} JSON string or null on failure */ stringify(value, options = {}) { const { pretty = false, replacer = null, defaultValue = null } = options; try { return JSON.stringify(value, replacer, pretty ? 2 : 0); } catch (error) { debugWarn('SafeJSON', 'Stringify failed:', error.message); // v8.25: gated return defaultValue; } }, /** * Attempt to repair common JSON errors * @param {string} str - Malformed JSON string * @returns {*} Parsed value or null if repair failed */ attemptRepair(str) { this.stats.repairAttempts++; const repairs = [ // Fix trailing commas s => s.replace(/,(\s*[}\]])/g, '$1'), // Fix single quotes s => s.replace(/'/g, '"'), // Fix unquoted keys s => s.replace(/(\{|\,)\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":'), // Fix missing quotes on string values (simple cases) s => s.replace(/:(\s*)([a-zA-Z][a-zA-Z0-9_]*)([\s,\}])/g, ':$1"$2"$3'), // Wrap in array if looks like multiple objects s => s.startsWith('{') && s.includes('}{') ? '[' + s.replace(/\}\{/g, '},{') + ']' : s, // Remove BOM s => s.replace(/^\uFEFF/, ''), // Trim whitespace s => s.trim() ]; let current = str; for (const repair of repairs) { try { current = repair(current); // Try parsing after each repair return JSON.parse(current); } catch (e) { // Continue trying repairs } } return null; }, /** * Deep clone an object via JSON * @param {*} value - Value to clone * @param {*} defaultValue - Default if clone fails * @returns {*} Cloned value or default */ clone(value, defaultValue = null) { const str = this.stringify(value); if (str === null) return defaultValue; return this.parse(str, defaultValue); }, /** * Parse from localStorage with safety * @param {string} key - localStorage key * @param {*} defaultValue - Default if not found or parse fails * @returns {*} Parsed value or default */ fromLocalStorage(key, defaultValue = null) { try { const str = localStorage.getItem(key); return this.parse(str, defaultValue); } catch (error) { debugWarn('SafeJSON', 'localStorage access failed:', error.message); // v8.25: gated return defaultValue; } }, /** * Save to localStorage safely * @param {string} key - localStorage key * @param {*} value - Value to save * @returns {boolean} Success status */ toLocalStorage(key, value) { try { const str = this.stringify(value); if (str === null) return false; localStorage.setItem(key, str); return true; } catch (error) { debugWarn('SafeJSON', 'localStorage save failed:', error.message); // v8.25: gated return false; } }, /** * Validate JSON structure against a schema * @param {*} data - Data to validate * @param {Object} schema - Simple schema { required: [], types: {} } * @returns {boolean} Validation result */ validate(data, schema) { if (!data || typeof data !== 'object') return false; // Check required fields if (schema.required) { for (const field of schema.required) { if (!(field in data)) return false; } } // Check types if (schema.types) { for (const [field, type] of Object.entries(schema.types)) { if (field in data) { const actualType = Array.isArray(data[field]) ? 'array' : typeof data[field]; if (actualType !== type) return false; } } } return true; }, /** * Get parsing statistics * @returns {Object} Statistics object */ getStats() { return { ...this.stats, successRate: this.stats.parseAttempts > 0 ? ((this.stats.parseSuccesses / this.stats.parseAttempts) * 100).toFixed(1) + '%' : 'N/A', repairRate: this.stats.repairAttempts > 0 ? ((this.stats.repairSuccesses / this.stats.repairAttempts) * 100).toFixed(1) + '%' : 'N/A' }; } }; // Make SafeJSON globally available window.SafeJSON = SafeJSON; debugLog('SafeJSON', 'Safe JSON parser utility initialized'); // v8.25: gated // ============================================ // THREE.js Extensions: FontLoader & TextGeometry // Required for 3D text rendering // ============================================ THREE.FontLoader = class FontLoader extends THREE.Loader { constructor(manager) { super(manager); } load(url, onLoad, onProgress, onError) { const scope = this; const loader = new THREE.FileLoader(this.manager); loader.setPath(this.path); loader.setRequestHeader(this.requestHeader); loader.setWithCredentials(this.withCredentials); loader.load(url, function(text) { try { const json = JSON.parse(text); const font = scope.parse(json); if (onLoad) onLoad(font); } catch (e) { if (onError) onError(e); } }, onProgress, onError); } parse(json) { return new THREE.Font(json); } }; THREE.Font = class Font { constructor(data) { this.type = 'Font'; this.data = data; } generateShapes(text, size = 100) { const shapes = []; const paths = createFontPaths(text, size, this.data); for (let p = 0, pl = paths.length; p < pl; p++) { Array.prototype.push.apply(shapes, paths[p].toShapes()); } return shapes; } }; THREE.TextGeometry = class TextGeometry extends THREE.ExtrudeGeometry { constructor(text, parameters = {}) { const font = parameters.font; if (!font || !font.data) { console.error('THREE.TextGeometry: font parameter is not an instance of THREE.Font.'); super(); return; } const shapes = font.generateShapes(text, parameters.size); parameters.depth = parameters.height !== undefined ? parameters.height : 50; if (parameters.bevelThickness === undefined) parameters.bevelThickness = 10; if (parameters.bevelSize === undefined) parameters.bevelSize = 8; if (parameters.bevelEnabled === undefined) parameters.bevelEnabled = false; super(shapes, parameters); this.type = 'TextGeometry'; } }; function createFontPaths(text, size, data) { const chars = Array.from(text); const scale = size / data.resolution; const line_height = (data.boundingBox.yMax - data.boundingBox.yMin + data.underlineThickness) * scale; const paths = []; let offsetX = 0, offsetY = 0; for (let i = 0; i < chars.length; i++) { const char = chars[i]; if (char === '\n') { offsetX = 0; offsetY -= line_height; } else { const ret = createFontPath(char, scale, offsetX, offsetY, data); if (ret) { offsetX += ret.offsetX; paths.push(ret.path); } } } return paths; } function createFontPath(char, scale, offsetX, offsetY, data) { const glyph = data.glyphs[char] || data.glyphs['?']; if (!glyph) { console.error('THREE.Font: character "' + char + '" does not exist in font.'); return; } const path = new THREE.ShapePath(); let x, y, cpx, cpy, cpx1, cpy1, cpx2, cpy2; if (glyph.o) { const outline = glyph._cachedOutline || (glyph._cachedOutline = glyph.o.split(' ')); for (let i = 0, l = outline.length; i < l;) { const action = outline[i++]; switch (action) { case 'm': x = outline[i++] * scale + offsetX; y = outline[i++] * scale + offsetY; path.moveTo(x, y); break; case 'l': x = outline[i++] * scale + offsetX; y = outline[i++] * scale + offsetY; path.lineTo(x, y); break; case 'q': cpx = outline[i++] * scale + offsetX; cpy = outline[i++] * scale + offsetY; cpx1 = outline[i++] * scale + offsetX; cpy1 = outline[i++] * scale + offsetY; path.quadraticCurveTo(cpx1, cpy1, cpx, cpy); break; case 'b': cpx = outline[i++] * scale + offsetX; cpy = outline[i++] * scale + offsetY; cpx1 = outline[i++] * scale + offsetX; cpy1 = outline[i++] * scale + offsetY; cpx2 = outline[i++] * scale + offsetX; cpy2 = outline[i++] * scale + offsetY; path.bezierCurveTo(cpx1, cpy1, cpx2, cpy2, cpx, cpy); break; } } } return { offsetX: glyph.ha * scale, path: path }; } // --- MODEL LOADER SYSTEM (v7.4: External model loading with caching) --- // Loads detailed 3D models from GitHub repo, caches locally for performance // Models are JSON-based hierarchical mesh definitions const ModelLoader = { // Configuration baseUrl: 'https://raw.githubusercontent.com/kody-w/localFirstTools/main/data/models', cacheVersion: '1.0.0', dbName: 'leviathan_model_cache', storeName: 'models', // Runtime state cache: new Map(), db: null, initialized: false, loading: new Map(), // Track in-flight loads to prevent duplicates // Initialize IndexedDB cache async init() { if (this.initialized) return; try { this.db = await new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, 1); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains(this.storeName)) { db.createObjectStore(this.storeName, { keyPath: 'id' }); } }; }); this.initialized = true; debugLog('ModelLoader', 'IndexedDB cache initialized'); // v8.25: gated } catch (e) { debugWarn('ModelLoader', 'IndexedDB not available, using memory cache only'); // v8.25: gated this.initialized = true; } }, // Get model from cache async getFromCache(modelId) { // Check memory cache first if (this.cache.has(modelId)) { return this.cache.get(modelId); } // Check IndexedDB if (this.db) { try { const data = await new Promise((resolve, reject) => { const tx = this.db.transaction(this.storeName, 'readonly'); const store = tx.objectStore(this.storeName); const request = store.get(modelId); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); if (data && data.version === this.cacheVersion) { this.cache.set(modelId, data.model); return data.model; } } catch (e) { debugWarn('ModelLoader', 'Cache read error:', e); // v8.25: gated } } return null; }, // Save model to cache async saveToCache(modelId, modelData) { this.cache.set(modelId, modelData); if (this.db) { try { await new Promise((resolve, reject) => { const tx = this.db.transaction(this.storeName, 'readwrite'); const store = tx.objectStore(this.storeName); const request = store.put({ id: modelId, version: this.cacheVersion, timestamp: Date.now(), model: modelData }); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } catch (e) { debugWarn('ModelLoader', 'Cache write error:', e); // v8.25: gated } } }, // Fetch model from GitHub async fetchModel(modelPath) { const url = `${this.baseUrl}/${modelPath}`; try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); return await response.json(); } catch (e) { debugWarn('ModelLoader', `Failed to fetch ${modelPath}:`, e); // v8.25: gated return null; } }, // Load a model by ID (with caching) async loadModel(modelId, modelPath) { await this.init(); // Check if already loading if (this.loading.has(modelId)) { return this.loading.get(modelId); } // Check cache const cached = await this.getFromCache(modelId); if (cached) { debugLog('ModelLoader', `Loaded ${modelId} from cache`); // v8.25: gated return cached; } // Fetch from GitHub const loadPromise = (async () => { const modelData = await this.fetchModel(modelPath); if (modelData) { await this.saveToCache(modelId, modelData); debugLog('ModelLoader', `Loaded ${modelId} from GitHub`); // v8.25: gated } this.loading.delete(modelId); return modelData; })(); this.loading.set(modelId, loadPromise); return loadPromise; }, // Build Three.js mesh from model definition buildMesh(modelData, options = {}) { if (!modelData || !modelData.parts) { debugWarn('ModelLoader', 'Invalid model data'); // v8.25: gated return null; } const group = new THREE.Group(); group.userData.modelName = modelData.name; group.userData.animations = modelData.animations || {}; group.userData.animatableParts = []; // Build each part recursively const buildPart = (partDef, parent) => { let mesh; // Create geometry based on type const geo = this.createGeometry(partDef.geometry); if (!geo) return null; // Create material const mat = this.createMaterial(partDef.material, options); mesh = new THREE.Mesh(geo, mat); mesh.userData.partId = partDef.id; mesh.userData.partName = partDef.name; // Apply position if (partDef.position) { mesh.position.set( partDef.position.x || 0, partDef.position.y || 0, partDef.position.z || 0 ); } // Apply rotation (in radians) if (partDef.rotation) { mesh.rotation.set( partDef.rotation.x || 0, partDef.rotation.y || 0, partDef.rotation.z || 0 ); } // Apply scale if (partDef.scale) { mesh.scale.set( partDef.scale.x || 1, partDef.scale.y || 1, partDef.scale.z || 1 ); } // Store animation data if (partDef.animate) { mesh.userData.animate = partDef.animate; group.userData.animatableParts.push(mesh); } mesh.castShadow = true; mesh.receiveShadow = true; // Add to parent parent.add(mesh); // Build children recursively if (partDef.children) { partDef.children.forEach(childDef => buildPart(childDef, mesh)); } return mesh; }; // Build all top-level parts modelData.parts.forEach(partDef => buildPart(partDef, group)); // Apply overall scale if specified if (options.scale) { group.scale.setScalar(options.scale); } // Apply team color tint if specified if (options.teamColor) { this.applyTeamColor(group, options.teamColor); } return group; }, // Create geometry from definition createGeometry(geoDef) { if (!geoDef) return new THREE.BoxGeometry(1, 1, 1); switch (geoDef.type) { case 'box': return new THREE.BoxGeometry( geoDef.width || 1, geoDef.height || 1, geoDef.depth || 1 ); case 'sphere': return new THREE.SphereGeometry( geoDef.radius || 0.5, geoDef.widthSegments || 16, geoDef.heightSegments || 12, geoDef.phiStart || 0, geoDef.phiLength || Math.PI * 2, geoDef.thetaStart || 0, geoDef.thetaLength || Math.PI ); case 'cylinder': return new THREE.CylinderGeometry( geoDef.radiusTop || 0.5, geoDef.radiusBottom || 0.5, geoDef.height || 1, geoDef.radialSegments || 16 ); case 'cone': return new THREE.ConeGeometry( geoDef.radius || 0.5, geoDef.height || 1, geoDef.radialSegments || 16 ); case 'torus': return new THREE.TorusGeometry( geoDef.radius || 0.5, geoDef.tube || 0.1, geoDef.radialSegments || 8, geoDef.tubularSegments || 24 ); case 'plane': return new THREE.PlaneGeometry( geoDef.width || 1, geoDef.height || 1 ); default: debugWarn('ModelLoader', `Unknown geometry type: ${geoDef.type}`); // v8.25: gated return new THREE.BoxGeometry(1, 1, 1); } }, // Create material from definition createMaterial(matDef, options = {}) { if (!matDef) { return new THREE.MeshStandardMaterial({ color: 0x888888 }); } const props = { color: new THREE.Color(matDef.color || '#888888'), metalness: matDef.metalness !== undefined ? matDef.metalness : 0.5, roughness: matDef.roughness !== undefined ? matDef.roughness : 0.5 }; // Emissive properties if (matDef.emissive) { props.emissive = new THREE.Color(matDef.emissive); props.emissiveIntensity = matDef.emissiveIntensity || 0.5; } // Transparency if (matDef.transparent || matDef.opacity !== undefined) { props.transparent = true; props.opacity = matDef.opacity !== undefined ? matDef.opacity : 1.0; } return new THREE.MeshStandardMaterial(props); }, // Apply team color tint to model applyTeamColor(group, color) { const tintColor = new THREE.Color(color); group.traverse(child => { if (child.isMesh && child.material) { // Blend team color with original const origColor = child.material.color.clone(); child.material.color.lerp(tintColor, 0.3); // Also tint emissive if present if (child.material.emissive) { child.material.emissive.lerp(tintColor, 0.5); } } }); }, // Animate model parts (call each frame) // v8.14: Converted forEach to for loop for hot path optimization animateModel(group, time, deltaTime) { if (!group.userData.animatableParts) return; const parts = group.userData.animatableParts; for (let pi = 0, pLen = parts.length; pi < pLen; pi++) { const part = parts[pi]; const anim = part.userData.animate; if (!anim) continue; switch (anim.type) { case 'rotate': part.rotation[anim.axis || 'y'] += (anim.speed || 1) * deltaTime; break; case 'pulse': if (part.material && part.material[anim.property] !== undefined) { const t = (Math.sin(time * (anim.speed || 1)) + 1) / 2; part.material[anim.property] = anim.min + (anim.max - anim.min) * t; } break; case 'flicker': if (part.material && part.material[anim.property] !== undefined) { const flicker = Math.random() > 0.1 ? 1 : 0.5; const base = anim.min + (anim.max - anim.min) * 0.5; part.material[anim.property] = base * flicker; } break; case 'sway': const swayAngle = Math.sin(time * (anim.speed || 1)) * (anim.amplitude || 0.1); part.rotation[anim.axis || 'z'] = swayAngle; break; case 'blink': if (part.material) { const blinkCycle = time % ((anim.onDuration || 0.1) + (anim.offDuration || 1)); part.material[anim.property || 'emissiveIntensity'] = blinkCycle < (anim.onDuration || 0.1) ? anim.onValue : anim.offValue; } break; case 'hover': const hoverOffset = Math.sin(time * (anim.speed || 1) + (anim.offset || 0)) * (anim.amplitude || 0.05); group.position.y = (group.userData.baseY || group.position.y) + hoverOffset; break; case 'walk': const walkAngle = Math.sin(time * (anim.speed || 1) + (anim.phase || 0)) * (anim.amplitude || 0.3); part.rotation.x = walkAngle; break; } } }, // Create fallback procedural mesh createFallbackMesh(type, team, options = {}) { const teamColor = team === 'A' ? 0x00ff88 : 0xff4444; if (type === 'robot-drone') { // Simple drone fallback const group = new THREE.Group(); const body = new THREE.Mesh( new THREE.BoxGeometry(0.8, 0.3, 0.8), new THREE.MeshStandardMaterial({ color: 0x2a4a5a, metalness: 0.8, roughness: 0.2 }) ); const core = new THREE.Mesh( new THREE.SphereGeometry(0.12), new THREE.MeshStandardMaterial({ color: teamColor, emissive: teamColor, emissiveIntensity: 1 }) ); core.position.y = 0.05; group.add(body); group.add(core); return group; } else { // Simple creature fallback const group = new THREE.Group(); const body = new THREE.Mesh( new THREE.SphereGeometry(0.3), new THREE.MeshStandardMaterial({ color: 0x4a2525, roughness: 0.7 }) ); body.scale.set(1, 0.7, 1.2); const eyes = new THREE.Mesh( new THREE.SphereGeometry(0.05), new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 2 }) ); eyes.position.set(0, 0.1, -0.25); group.add(body); group.add(eyes); return group; } }, // Clear cache (for debugging/updates) async clearCache() { this.cache.clear(); if (this.db) { try { await new Promise((resolve, reject) => { const tx = this.db.transaction(this.storeName, 'readwrite'); const store = tx.objectStore(this.storeName); const request = store.clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); debugLog('ModelLoader', 'Cache cleared'); // v8.25: gated } catch (e) { debugWarn('ModelLoader', 'Failed to clear cache:', e); // v8.25: gated } } } }; // Pre-load commonly used models async function preloadModels() { debugLog('ModelLoader', 'Preloading models...'); // v8.25: gated await ModelLoader.init(); // Load creep models in background ModelLoader.loadModel('robot-drone', 'creeps/robot-drone.json'); ModelLoader.loadModel('hostile-fauna-basic', 'creeps/hostile-fauna-basic.json'); } // Start preloading preloadModels(); // --- AUDIO SYSTEM (Web Audio API - No external dependencies) --- // v6.14: THERAPEUTIC AUDIO REDESIGN // Designed for pleasant background listening while multitasking // - Pentatonic scales (always harmonious, no dissonance) // - Soft sine waves with gentle envelopes // - Musical intervals that are pleasing to hear repeatedly // - State-conveying ambient layers (health, prosperity, danger) // v7.28: Centralized AudioContext Manager (8-Strategy Consensus Cycle 1) // Prevents multiple AudioContext instances which can cause browser throttling let _sharedAudioContext = null; function getSharedAudioContext() { if (!_sharedAudioContext) { try { _sharedAudioContext = new (window.AudioContext || window.webkitAudioContext)(); Logger.info('AudioContextManager', 'Created shared AudioContext'); } catch (e) { Logger.warn('AudioContextManager', 'Failed to create AudioContext:', e); return null; } } // Resume if suspended (iOS/Safari requirement) if (_sharedAudioContext.state === 'suspended') { _sharedAudioContext.resume().catch(() => {}); } return _sharedAudioContext; } const AudioSystem = { ctx: null, enabled: true, masterVolume: 0.2, // Lower for background listening sfxMultiplier: 1.0, // v10.20: Individual SFX volume (Audio Mixer) ambientMultiplier: 1.0, // v10.20: Individual ambient volume (Audio Mixer) // v6.14: Pentatonic scale - these notes NEVER clash penta: { C3: 130.81, D3: 146.83, E3: 164.81, G3: 196.00, A3: 220.00, C4: 261.63, D4: 293.66, E4: 329.63, G4: 392.00, A4: 440.00, C5: 523.25, D5: 587.33, E5: 659.25, G5: 783.99, A5: 880.00 }, init() { // v7.28: Use shared AudioContext this.ctx = getSharedAudioContext(); if (!this.ctx) { Logger.warn('AudioSystem', 'Web Audio API not supported'); this.enabled = false; } }, resume() { if (this.ctx && this.ctx.state === 'suspended') { this.ctx.resume(); } }, // v6.14: Gentle tone with soft attack/decay (therapeutic) playGentle(freq, dur, vol = 0.3) { if (!this.enabled || !this.ctx) return; this.resume(); const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); osc.type = 'sine'; osc.frequency.value = freq; filter.type = 'lowpass'; filter.frequency.value = 1500; const now = this.ctx.currentTime; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(this.masterVolume * vol, now + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, now + dur); osc.connect(filter).connect(gain).connect(this.ctx.destination); osc.start(now); osc.stop(now + dur); }, // Legacy compatibility - routes through gentle system playTone(freq, duration, type = 'sine', volume = 1) { this.playGentle(freq, duration * 1.5, volume * 0.5); }, // v6.6: Generic play() method play(soundName) { const soundMap = { 'hit': () => this.hit(), 'collect': () => this.collect(), 'damage': () => this.damage(), 'kill': () => this.defeat(), 'defeat': () => this.defeat(), 'explosion': () => this.explosion(), 'levelUp': () => this.levelUp(), 'craft': () => this.craft(), 'click': () => this.click(), 'error': () => this.gentleWarn(), 'heal': () => this.heal(), 'dodge': () => this.dodge(), 'telegraph': () => this.telegraph(), 'spell': () => this.playGentle(this.penta.E4, 0.4, 0.25), 'powerup': () => this.levelUp(), 'ui': () => this.click(), 'bossSpawn': () => this.bossSpawn(), 'recipeDiscovered': () => this.recipeDiscovered() }; (soundMap[soundName] || (() => this.playGentle(this.penta.C4, 0.3, 0.15)))(); }, // v6.14: THERAPEUTIC SOUND EFFECTS // v6.41: Combo-aware hit sound with ascending pitch (Agent 5 consensus - audio juice) // v10.0: Enhanced hit sound with micro-variations and impact layers (8-Agent Consensus Cycle 8) hit(comboCount = 0) { if (!this.enabled || !this.ctx) return; this.resume(); // Scale up pentatonic notes as combo builds - creates satisfying musical feedback const pitchSteps = [this.penta.G3, this.penta.A3, this.penta.C4, this.penta.D4, this.penta.E4]; const clampedCombo = Math.min(comboCount, pitchSteps.length - 1); const basePitch = pitchSteps[clampedCombo]; // v10.0: Micro-variations for organic feel (avoid robotic repetition) const pitchVar = 1 + (Math.random() - 0.5) * 0.04; // ±2% pitch const timingVar = Math.random() * 8; // 0-8ms timing offset // Volume and duration increase slightly with combo const vol = 0.2 + clampedCombo * 0.025; const dur = 0.12 + clampedCombo * 0.015; // Primary tone with variation setTimeout(() => this.playGentle(basePitch * pitchVar, dur, vol), timingVar); // v10.0: Impact "click" layer - adds percussive attack this._playImpactClick(vol * 0.6, clampedCombo); // Add harmonic overtone for higher combos (satisfying stacking effect) if (comboCount >= 3) { const harmDelay = 20 + Math.random() * 10; setTimeout(() => this.playGentle(basePitch * 1.5, 0.08, 0.06), harmDelay); } // v10.0: Sub-bass thump for high combos if (comboCount >= 4) { this._playSubThump(vol * 0.4); } }, // v10.0: Percussive attack click for hit confirmation _playImpactClick(vol, intensity) { if (!this.ctx) return; try { const now = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'square'; osc.frequency.value = 1200 + intensity * 200 + Math.random() * 100; gain.gain.setValueAtTime(this.masterVolume * vol, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.025); osc.connect(gain).connect(this.ctx.destination); osc.start(now); osc.stop(now + 0.03); } catch(e) {} }, // v10.0: Sub-bass thump for powerful hits _playSubThump(vol) { if (!this.ctx) return; try { const now = this.ctx.currentTime; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(80, now); osc.frequency.exponentialRampToValueAtTime(40, now + 0.08); gain.gain.setValueAtTime(this.masterVolume * vol, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1); osc.connect(gain).connect(this.ctx.destination); osc.start(now); osc.stop(now + 0.12); } catch(e) {} }, collect() { this.playGentle(this.penta.E4, 0.25, 0.25); setTimeout(() => this.playGentle(this.penta.G4, 0.3, 0.15), 60); }, damage() { this.playGentle(this.penta.C3, 0.2, 0.3); }, defeat() { // Satisfying chord this.playGentle(this.penta.C4, 0.4, 0.25); setTimeout(() => this.playGentle(this.penta.E4, 0.35, 0.2), 30); setTimeout(() => this.playGentle(this.penta.G4, 0.3, 0.15), 60); }, kill() { this.defeat(); }, explosion() { this.playGentle(this.penta.C3, 0.5, 0.35); setTimeout(() => this.playGentle(this.penta.G3, 0.4, 0.2), 100); }, levelUp() { [this.penta.C4, this.penta.E4, this.penta.G4, this.penta.C5].forEach((f, i) => { setTimeout(() => this.playGentle(f, 0.35, 0.3 - i * 0.04), i * 100); }); }, // v7.32: Unique parry sound - metallic "deflect" (8-Strategy Cycle 11 Consensus) // High-skill mechanic deserves distinct audio identity parry() { if (!this.enabled || !this.ctx) return; this.resume(); const now = this.ctx.currentTime; // Metallic "clang" - dual detuned square waves const osc1 = this.ctx.createOscillator(); const osc2 = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); osc1.type = 'square'; osc2.type = 'square'; osc1.frequency.value = 800; osc2.frequency.value = 850; // Slight detune for metallic shimmer filter.type = 'bandpass'; filter.frequency.value = 2000; filter.Q.value = 4; gain.gain.setValueAtTime(this.masterVolume * 0.35, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15); osc1.connect(filter); osc2.connect(filter); filter.connect(gain).connect(this.ctx.destination); osc1.start(now); osc2.start(now); osc1.stop(now + 0.2); osc2.stop(now + 0.2); // Rising "power" tone after successful parry setTimeout(() => { this.playGentle(this.penta.E4, 0.2, 0.2); setTimeout(() => this.playGentle(this.penta.A4, 0.25, 0.15), 50); }, 80); }, craft() { this.playGentle(this.penta.A4, 0.2, 0.2); setTimeout(() => this.playGentle(this.penta.E5, 0.25, 0.12), 50); }, // v7.59: Shield block impact audio (Evolution Cycle 2 - Audio/Game Feel Consensus) // Deep resonant "clank" - heavier than parry, conveys solidity shieldBlock(damageBlocked) { if (!this.enabled || !this.ctx) return; this.resume(); const now = this.ctx.currentTime; const osc1 = this.ctx.createOscillator(); const osc2 = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); osc1.type = 'triangle'; osc2.type = 'sine'; osc1.frequency.value = 180; // Lower than parry for "heavier" feel osc2.frequency.value = 90; // Sub-octave reinforcement filter.type = 'lowpass'; filter.frequency.value = 800; filter.Q.value = 2; // Volume scales slightly with damage blocked for satisfaction const vol = Math.min(0.4, 0.25 + ((damageBlocked || 10) / 100) * 0.15); gain.gain.setValueAtTime(this.masterVolume * vol, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25); osc1.connect(filter); osc2.connect(filter); filter.connect(gain).connect(this.ctx.destination); osc1.start(now); osc2.start(now); osc1.stop(now + 0.3); osc2.stop(now + 0.3); }, click() { this.playGentle(this.penta.C5, 0.06, 0.08); }, gentleWarn() { this.playGentle(this.penta.E4, 0.25, 0.2); setTimeout(() => this.playGentle(this.penta.D4, 0.3, 0.15), 120); }, error() { this.gentleWarn(); }, heal() { this.playGentle(this.penta.G4, 0.4, 0.25); setTimeout(() => this.playGentle(this.penta.C5, 0.35, 0.2), 80); }, dodge() { this.playGentle(this.penta.D4, 0.1, 0.15); setTimeout(() => this.playGentle(this.penta.G4, 0.08, 0.1), 30); }, telegraph() { this.playGentle(this.penta.A3, 0.18, 0.15); }, // v6.68: War horn for versus match start - epic, powerful sound warHorn() { if (!this.enabled || !this.ctx) return; this.resume(); // Create a deep, resonant horn sound const now = this.ctx.currentTime; // Main horn tone - deep and powerful const horn1 = this.ctx.createOscillator(); const horn1Gain = this.ctx.createGain(); horn1.type = 'sawtooth'; horn1.frequency.setValueAtTime(65.41, now); // C2 horn1.frequency.linearRampToValueAtTime(73.42, now + 0.3); // Rise slightly horn1.frequency.linearRampToValueAtTime(65.41, now + 2.5); // Back down horn1Gain.gain.setValueAtTime(0, now); horn1Gain.gain.linearRampToValueAtTime(this.masterVolume * 0.4, now + 0.2); horn1Gain.gain.setValueAtTime(this.masterVolume * 0.4, now + 2); horn1Gain.gain.linearRampToValueAtTime(0, now + 2.8); // Harmonic overtone const horn2 = this.ctx.createOscillator(); const horn2Gain = this.ctx.createGain(); horn2.type = 'sawtooth'; horn2.frequency.setValueAtTime(130.81, now); // C3 horn2.frequency.linearRampToValueAtTime(146.83, now + 0.3); horn2.frequency.linearRampToValueAtTime(130.81, now + 2.5); horn2Gain.gain.setValueAtTime(0, now); horn2Gain.gain.linearRampToValueAtTime(this.masterVolume * 0.25, now + 0.2); horn2Gain.gain.setValueAtTime(this.masterVolume * 0.25, now + 2); horn2Gain.gain.linearRampToValueAtTime(0, now + 2.8); // Sub bass for power const sub = this.ctx.createOscillator(); const subGain = this.ctx.createGain(); sub.type = 'sine'; sub.frequency.value = 32.7; // C1 subGain.gain.setValueAtTime(0, now); subGain.gain.linearRampToValueAtTime(this.masterVolume * 0.5, now + 0.15); subGain.gain.setValueAtTime(this.masterVolume * 0.5, now + 2); subGain.gain.linearRampToValueAtTime(0, now + 2.8); // Lowpass filter for warmth const filter = this.ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 800; filter.Q.value = 1; // Connect horn1.connect(horn1Gain).connect(filter); horn2.connect(horn2Gain).connect(filter); sub.connect(subGain).connect(filter); filter.connect(this.ctx.destination); horn1.start(now); horn1.stop(now + 3); horn2.start(now); horn2.stop(now + 3); sub.start(now); sub.stop(now + 3); }, // v6.68: Victory fanfare for winning versus match victoryFanfare() { // Ascending triumphant melody const notes = [this.penta.C4, this.penta.E4, this.penta.G4, this.penta.C5, this.penta.E5]; notes.forEach((f, i) => { setTimeout(() => this.playGentle(f, 0.5, 0.35), i * 120); }); // Final chord setTimeout(() => { this.playGentle(this.penta.C5, 0.8, 0.3); this.playGentle(this.penta.E5, 0.8, 0.25); this.playGentle(this.penta.G5, 0.8, 0.2); }, 600); }, // v6.68: Defeat sound for losing versus match defeatSound() { // Descending somber melody const notes = [this.penta.G4, this.penta.E4, this.penta.C4, this.penta.G3, this.penta.C3]; notes.forEach((f, i) => { setTimeout(() => this.playGentle(f, 0.6, 0.25 - i * 0.03), i * 200); }); }, // v6.14: Calmer heartbeat - meditation pulse, not panic // v6.33: HEARTBEAT WORLD PULSE - 8-agent consensus synaesthetic effect heartbeatInterval: null, heartbeatActive: false, heartbeatVisualCallback: null, startHeartbeat(hpPercent) { if (!this.enabled || !this.ctx || this.heartbeatActive) return; this.resume(); this.heartbeatActive = true; const bpm = 35 + (1 - hpPercent) * 15; // 35-50 BPM (meditative) const playBeat = () => { if (!this.heartbeatActive) return; this.playGentle(this.penta.C3, 0.35, 0.12); // v6.33: Trigger visual world pulse synchronized with heartbeat if (this.heartbeatVisualCallback) { this.heartbeatVisualCallback(hpPercent); } }; playBeat(); // v7.38: Migrate heartbeat to TimerRegistry (Cycle 17 Code Quality) TimerRegistry.setInterval('audio-heartbeat', playBeat, 60000 / bpm); }, stopHeartbeat() { // v7.38: Use TimerRegistry for centralized timer management TimerRegistry.clearInterval('audio-heartbeat'); this.heartbeatActive = false; }, updateHeartbeat(hpPercent) { if (hpPercent <= 0.25 && !this.heartbeatActive) this.startHeartbeat(hpPercent); else if (hpPercent > 0.25 && this.heartbeatActive) this.stopHeartbeat(); }, // v6.14: Therapeutic ambient - layered harmonic drones ambientNodes: null, currentBiome: null, biomeAmbient: { Terra: { base: 65.41, harmonics: [1, 1.5, 2], vol: 0.04 }, Desert: { base: 73.42, harmonics: [1, 1.33, 2], vol: 0.035 }, Ice: { base: 98.00, harmonics: [1, 1.5, 2.5], vol: 0.035 }, Volcanic: { base: 55.00, harmonics: [1, 1.25, 1.5], vol: 0.04 }, Alien: { base: 82.41, harmonics: [1, 1.4, 2.2], vol: 0.03 } }, // v7.61: Extracted fade duration constant (Cycle 39 Code Quality) AMBIENT_FADE_DURATION: 1.5, // v10.1: BIOME AMBIENT CROSSFADE (8-Agent Consensus Cycle 2) startAmbient(biome) { if (!this.enabled || !this.ctx || this.currentBiome === biome) return; this.resume(); const fadeDur = this.AMBIENT_FADE_DURATION; const now = this.ctx.currentTime; // Crossfade: fade out old biome ambient if (this.ambientNodes) { const oldNodes = this.ambientNodes; oldNodes.forEach(n => { try { n.gain.gain.setValueAtTime(n.gain.gain.value, now); n.gain.gain.linearRampToValueAtTime(0, now + fadeDur); } catch(e) {} }); setTimeout(() => { oldNodes.forEach(n => { try { n.osc.stop(); n.osc.disconnect(); n.lfo.stop(); n.lfo.disconnect(); } catch(e) {} }); }, fadeDur * 1000 + 100); } this.currentBiome = biome; const cfg = this.biomeAmbient[biome] || this.biomeAmbient.Terra; this.ambientNodes = []; cfg.harmonics.forEach((h, i) => { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); const lfo = this.ctx.createOscillator(); const lfoG = this.ctx.createGain(); lfo.frequency.value = 0.08 + i * 0.03; lfoG.gain.value = cfg.base * h * 0.015; lfo.connect(lfoG).connect(osc.frequency); lfo.start(); osc.type = 'sine'; osc.frequency.value = cfg.base * h; filter.type = 'lowpass'; filter.frequency.value = 350 - i * 40; // Crossfade: fade in new biome ambient const targetVol = cfg.vol * this.masterVolume * (1 - i * 0.2); gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(targetVol, now + fadeDur); osc.connect(filter).connect(gain).connect(this.ctx.destination); osc.start(); this.ambientNodes.push({ osc, gain, lfo, filter, targetVol }); }); }, stopAmbient() { if (this.ambientNodes && this.ctx) { const fadeDur = this.AMBIENT_FADE_DURATION; const now = this.ctx.currentTime; const oldNodes = this.ambientNodes; // Crossfade: fade out instead of abrupt stop oldNodes.forEach(n => { try { n.gain.gain.setValueAtTime(n.gain.gain.value, now); n.gain.gain.linearRampToValueAtTime(0, now + fadeDur); } catch(e) {} }); this.ambientNodes = null; this.currentBiome = null; setTimeout(() => { oldNodes.forEach(n => { try { n.osc.stop(); n.osc.disconnect(); n.lfo.stop(); n.lfo.disconnect(); } catch(e) {} }); }, fadeDur * 1000 + 100); } }, bossSpawn() { [this.penta.C3, this.penta.G3, this.penta.C3].forEach((f, i) => { setTimeout(() => this.playGentle(f, 0.6, 0.3 - i * 0.06), i * 180); }); }, recipeDiscovered() { [this.penta.E4, this.penta.G4, this.penta.A4].forEach((f, i) => { setTimeout(() => this.playGentle(f, 0.3, 0.25), i * 80); }); }, // v10.4: MYSTERY AUDIO METHOD (8-Agent Consensus Cycle 5) // Ethereal shimmer for temporal echo discovery - fixes broken call mystery() { if (!this.enabled || !this.ctx) return; this.resume(); const now = this.ctx.currentTime; const baseFreqs = [this.penta.E4, this.penta.G4, this.penta.C5]; baseFreqs.forEach((baseFreq, i) => { setTimeout(() => { try { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const lfo = this.ctx.createOscillator(); const lfoGain = this.ctx.createGain(); // LFO for ethereal tremolo lfo.frequency.value = 4; lfoGain.gain.value = 0.15; lfo.connect(lfoGain).connect(osc.frequency); lfo.start(now); osc.type = 'sine'; osc.frequency.value = baseFreq; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(this.masterVolume * 0.2, now + 0.08); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45); osc.connect(gain).connect(this.ctx.destination); osc.start(now); osc.stop(now + 0.5); // Harmonic shimmer layer const harmOsc = this.ctx.createOscillator(); const harmGain = this.ctx.createGain(); harmOsc.type = 'sine'; harmOsc.frequency.value = baseFreq * 2; harmGain.gain.setValueAtTime(0, now); harmGain.gain.linearRampToValueAtTime(this.masterVolume * 0.08, now + 0.1); harmGain.gain.exponentialRampToValueAtTime(0.001, now + 0.5); harmOsc.connect(harmGain).connect(this.ctx.destination); harmOsc.start(now); harmOsc.stop(now + 0.5); lfo.stop(now + 0.5); } catch(e) {} }, i * 120); }); }, // v6.14: New therapeutic sounds for background awareness agentPing() { this.playGentle(this.penta.A5, 0.15, 0.08); }, prosperity() { this.playGentle(this.penta.C5, 0.25, 0.12); }, waveSpawn() { this.playGentle(this.penta.G3, 0.4, 0.1); }, // ═══════════════════════════════════════════════════════════════ // v6.15: NATURE SOUNDSCAPE SYSTEM // Procedural birds, wind through grass, natural ambience // Creates immersive outdoor atmosphere without audio loops // ═══════════════════════════════════════════════════════════════ natureActive: false, birdTimeouts: [], windNodes: null, cricketInterval: null, // Bird species - each has unique frequency patterns and timing birdSpecies: { // Warbler: Quick ascending trill (cheerful, common) warbler: { notes: [2200, 2400, 2600, 2800, 3000], noteDur: 0.04, gap: 30, vol: 0.06, chance: 0.4 }, // Thrush: Descending melodic phrase (peaceful, forest) thrush: { notes: [1800, 1650, 1500, 1400, 1300], noteDur: 0.12, gap: 100, vol: 0.05, chance: 0.25 }, // Sparrow: Simple repeated chips (friendly, familiar) sparrow: { notes: [3200, 3200, 3400], noteDur: 0.05, gap: 60, vol: 0.045, chance: 0.35 }, // Robin: Musical phrase with pause (dawn chorus feel) robin: { notes: [2000, 2400, 2200, 2600, 2400, 2800], noteDur: 0.08, gap: 70, vol: 0.055, chance: 0.2 }, // Chickadee: Distinctive two-tone call chickadee: { notes: [2800, 2300, 2300], noteDur: 0.15, gap: 120, vol: 0.05, chance: 0.2 }, // Mourning dove: Soft cooing (calming) dove: { notes: [600, 800, 700, 700, 600], noteDur: 0.25, gap: 200, vol: 0.04, chance: 0.15 } }, // Biome-specific nature configurations biomeNature: { Terra: { birdDensity: 1.0, // Full bird activity birdTypes: ['warbler', 'thrush', 'sparrow', 'robin', 'chickadee', 'dove'], windBase: 0.015, // Gentle breeze windGust: 0.025, crickets: true, cricketVol: 0.02 }, Desert: { birdDensity: 0.3, // Sparse birds birdTypes: ['sparrow', 'dove'], windBase: 0.025, // Stronger wind windGust: 0.04, crickets: false, cricketVol: 0 }, Ice: { birdDensity: 0.1, // Almost no birds birdTypes: ['sparrow'], windBase: 0.008, // Subtle cold breeze (not harsh) windGust: 0.015, crickets: false, cricketVol: 0 }, Volcanic: { birdDensity: 0.05, // Minimal life birdTypes: [], windBase: 0.02, windGust: 0.03, crickets: false, cricketVol: 0 }, Alien: { birdDensity: 0.4, // Alien creatures birdTypes: ['alien'], // Special alien calls windBase: 0.018, windGust: 0.03, crickets: false, cricketVol: 0 }, // v7.24: FACTORY INDUSTRIAL SOUNDSCAPE (8-Strategy Consensus) Factory: { birdDensity: 0, // No birds in industrial setting birdTypes: [], // Replaced by machinery sounds windBase: 0.006, // Minimal - indoor/sheltered windGust: 0.01, crickets: false, cricketVol: 0, // Industrial-specific ambient machineryHum: true, humFreq: 60, // 60Hz electrical hum humVolume: 0.012 } }, // Play a single bird chirp with natural variation chirpBird(species) { if (!this.enabled || !this.ctx || !this.natureActive) return; this.resume(); const bird = species === 'alien' ? { // Alien creature: weird sliding tones notes: [800 + Math.random() * 400, 1200 + Math.random() * 600, 600 + Math.random() * 300], noteDur: 0.15 + Math.random() * 0.1, gap: 80, vol: 0.04 } : this.birdSpecies[species]; if (!bird) return; // Add natural pitch variation (±5%) const pitchVar = 0.95 + Math.random() * 0.1; // Add timing variation const tempoVar = 0.85 + Math.random() * 0.3; bird.notes.forEach((freq, i) => { setTimeout(() => { if (!this.natureActive) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); // Bird calls use sine for purity osc.type = 'sine'; osc.frequency.value = freq * pitchVar; // Slight frequency wobble for natural sound const vibrato = this.ctx.createOscillator(); const vibGain = this.ctx.createGain(); vibrato.frequency.value = 8 + Math.random() * 4; vibGain.gain.value = freq * 0.015; vibrato.connect(vibGain).connect(osc.frequency); vibrato.start(); // Bandpass to make it sound more like a bird filter.type = 'bandpass'; filter.frequency.value = freq * pitchVar; filter.Q.value = 3; const now = this.ctx.currentTime; const dur = bird.noteDur * tempoVar; // Natural attack/decay envelope gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(this.masterVolume * bird.vol, now + dur * 0.15); gain.gain.setValueAtTime(this.masterVolume * bird.vol, now + dur * 0.6); gain.gain.exponentialRampToValueAtTime(0.001, now + dur); osc.connect(filter).connect(gain).connect(this.ctx.destination); osc.start(now); osc.stop(now + dur); vibrato.stop(now + dur); }, i * bird.gap * tempoVar); }); }, // Schedule random bird chirps with natural clustering scheduleBirdChirps(biome) { if (!this.natureActive) return; const cfg = this.biomeNature[biome] || this.biomeNature.Terra; if (cfg.birdTypes.length === 0) return; // Random delay: 2-8 seconds, adjusted by density const baseDelay = 2000 + Math.random() * 6000; const delay = baseDelay / cfg.birdDensity; const timeout = setTimeout(() => { if (!this.natureActive) return; // Pick random bird species const species = cfg.birdTypes[Math.floor(Math.random() * cfg.birdTypes.length)]; const bird = this.birdSpecies[species]; // Check if this bird "wants" to sing based on its chance if (!bird || Math.random() < bird.chance) { this.chirpBird(species); // Sometimes birds respond to each other (clustering) if (Math.random() < 0.3 && cfg.birdTypes.length > 1) { setTimeout(() => { const otherSpecies = cfg.birdTypes.filter(s => s !== species); if (otherSpecies.length > 0) { this.chirpBird(otherSpecies[Math.floor(Math.random() * otherSpecies.length)]); } }, 300 + Math.random() * 700); } } // Schedule next chirp this.scheduleBirdChirps(biome); }, delay); this.birdTimeouts.push(timeout); }, // Wind through grass - filtered noise with gentle modulation startWind(biome) { if (!this.enabled || !this.ctx) return; this.resume(); const cfg = this.biomeNature[biome] || this.biomeNature.Terra; // Create noise source using buffer const bufferSize = this.ctx.sampleRate * 2; const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); const output = noiseBuffer.getChannelData(0); // Pink-ish noise (more natural than white) let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0; for (let i = 0; i < bufferSize; i++) { const white = Math.random() * 2 - 1; b0 = 0.99886 * b0 + white * 0.0555179; b1 = 0.99332 * b1 + white * 0.0750759; b2 = 0.96900 * b2 + white * 0.1538520; b3 = 0.86650 * b3 + white * 0.3104856; b4 = 0.55000 * b4 + white * 0.5329522; b5 = -0.7616 * b5 - white * 0.0168980; output[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362) * 0.11; b6 = white * 0.115926; } const noise = this.ctx.createBufferSource(); noise.buffer = noiseBuffer; noise.loop = true; // Multiple filters for wind character const lowpass = this.ctx.createBiquadFilter(); lowpass.type = 'lowpass'; lowpass.frequency.value = 400; // Low rumble const highpass = this.ctx.createBiquadFilter(); highpass.type = 'highpass'; highpass.frequency.value = 60; // Main gain const gain = this.ctx.createGain(); gain.gain.value = this.masterVolume * cfg.windBase; // LFO for wind swells (breathing effect) const lfo1 = this.ctx.createOscillator(); const lfo1Gain = this.ctx.createGain(); lfo1.frequency.value = 0.08; // Very slow swell lfo1Gain.gain.value = this.masterVolume * cfg.windBase * 0.5; lfo1.connect(lfo1Gain).connect(gain.gain); // Second LFO for irregular gusts const lfo2 = this.ctx.createOscillator(); const lfo2Gain = this.ctx.createGain(); lfo2.frequency.value = 0.03; lfo2Gain.gain.value = this.masterVolume * cfg.windGust * 0.3; lfo2.connect(lfo2Gain).connect(gain.gain); // Higher frequency component for grass rustle const highNoise = this.ctx.createBufferSource(); highNoise.buffer = noiseBuffer; highNoise.loop = true; const grassFilter = this.ctx.createBiquadFilter(); grassFilter.type = 'bandpass'; grassFilter.frequency.value = 2000; grassFilter.Q.value = 0.5; const grassGain = this.ctx.createGain(); grassGain.gain.value = this.masterVolume * cfg.windBase * 0.15; // Grass rustle modulation const grassLfo = this.ctx.createOscillator(); const grassLfoGain = this.ctx.createGain(); grassLfo.frequency.value = 0.12; grassLfoGain.gain.value = this.masterVolume * cfg.windBase * 0.1; grassLfo.connect(grassLfoGain).connect(grassGain.gain); // Connect everything noise.connect(lowpass).connect(highpass).connect(gain).connect(this.ctx.destination); highNoise.connect(grassFilter).connect(grassGain).connect(this.ctx.destination); // Start all oscillators noise.start(); highNoise.start(); lfo1.start(); lfo2.start(); grassLfo.start(); this.windNodes = { noise, highNoise, lowpass, highpass, gain, grassFilter, grassGain, lfo1, lfo1Gain, lfo2, lfo2Gain, grassLfo, grassLfoGain }; }, stopWind() { if (this.windNodes) { try { this.windNodes.noise.stop(); this.windNodes.highNoise.stop(); this.windNodes.lfo1.stop(); this.windNodes.lfo2.stop(); this.windNodes.grassLfo.stop(); Object.values(this.windNodes).forEach(n => { try { n.disconnect(); } catch(e) {} }); } catch(e) {} this.windNodes = null; } }, // Cricket/night sounds for appropriate biomes // v7.72: Use TimerRegistry for proper cleanup tracking startCrickets(biome) { const cfg = this.biomeNature[biome] || this.biomeNature.Terra; if (!cfg.crickets) return; const self = this; const cricketFn = () => { if (!self.natureActive || !self.ctx) return; // Random chance for cricket chirp if (Math.random() < 0.4) { const baseFreq = 4000 + Math.random() * 500; const chirps = 2 + Math.floor(Math.random() * 4); for (let i = 0; i < chirps; i++) { setTimeout(() => { if (!self.natureActive) return; const osc = self.ctx.createOscillator(); const gain = self.ctx.createGain(); osc.type = 'sine'; osc.frequency.value = baseFreq + Math.random() * 100; const now = self.ctx.currentTime; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(self.masterVolume * cfg.cricketVol, now + 0.005); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.03); osc.connect(gain).connect(self.ctx.destination); osc.start(now); osc.stop(now + 0.03); }, i * 50); } } }; const interval = 800 + Math.random() * 1200; if (typeof TimerRegistry !== 'undefined') { TimerRegistry.setInterval('audio-crickets', cricketFn, interval); } else { this.cricketInterval = setInterval(cricketFn, interval); } }, stopCrickets() { if (typeof TimerRegistry !== 'undefined') { TimerRegistry.clear('audio-crickets'); } else if (this.cricketInterval) { clearInterval(this.cricketInterval); this.cricketInterval = null; } }, // Master nature soundscape control startNatureSoundscape(biome) { if (this.natureActive && this.currentBiome === biome) return; this.stopNatureSoundscape(); this.natureActive = true; this.startWind(biome); this.scheduleBirdChirps(biome); this.startCrickets(biome); }, stopNatureSoundscape() { this.natureActive = false; // Clear all bird timeouts this.birdTimeouts.forEach(t => clearTimeout(t)); this.birdTimeouts = []; this.stopWind(); this.stopCrickets(); }, // Override startAmbient to include nature sounds _originalStartAmbient: null, initNature() { // Wrap startAmbient to also start nature sounds const self = this; const originalStart = this.startAmbient.bind(this); this.startAmbient = function(biome) { originalStart(biome); self.startNatureSoundscape(biome); }; const originalStop = this.stopAmbient.bind(this); this.stopAmbient = function() { originalStop(); self.stopNatureSoundscape(); }; }, // ═══════════════════════════════════════════════════════════════ // v7.23: BIOME-AWARE FOOTSTEPS AUDIO SYSTEM (8-Strategy Consensus Cycle 8) // Adds grounding audio for player movement // Different sounds per biome (grass, sand, snow, metal, volcanic rock) // ═══════════════════════════════════════════════════════════════ footsteps: { enabled: true, lastStep: 0, stepInterval: 350, // ms between footstep sounds isMoving: false, currentBiome: 'Terra', // v7.24: Pre-generated noise buffers for performance (8-Strategy Consensus Cycle 9) noiseBufferPool: {}, poolSize: 4, // 4 variations per biome to avoid repetition poolInitialized: false, // Biome-specific footstep configurations biomeConfig: { Terra: { baseFreq: 120, noiseAmount: 0.15, duration: 0.08, filterFreq: 800, volume: 0.12, type: 'grass' // grass rustling sound }, Desert: { baseFreq: 200, noiseAmount: 0.25, duration: 0.06, filterFreq: 2000, volume: 0.15, type: 'sand' // soft sand crunch }, Ice: { baseFreq: 350, noiseAmount: 0.08, duration: 0.1, filterFreq: 3500, volume: 0.18, type: 'snow' // crisp snow crunch }, Volcanic: { baseFreq: 80, noiseAmount: 0.2, duration: 0.12, filterFreq: 600, volume: 0.2, type: 'rock' // gravelly rock sound }, Factory: { baseFreq: 250, noiseAmount: 0.05, duration: 0.05, filterFreq: 1500, volume: 0.15, type: 'metal' // metallic clank }, Alien: { baseFreq: 180, noiseAmount: 0.3, duration: 0.09, filterFreq: 1200, volume: 0.12, type: 'organic' // squelchy organic sound } }, // v7.24: Pre-generate noise buffers on first use (lazy initialization) initBufferPool() { if (this.poolInitialized || !AudioSystem.ctx) return; const ctx = AudioSystem.ctx; for (const [biomeName, cfg] of Object.entries(this.biomeConfig)) { this.noiseBufferPool[biomeName] = []; const bufferSize = Math.ceil(ctx.sampleRate * cfg.duration); // Create pool of variations for natural variety for (let v = 0; v < this.poolSize; v++) { const noiseBuffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate); const output = noiseBuffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { output[i] = (Math.random() * 2 - 1) * cfg.noiseAmount; } this.noiseBufferPool[biomeName].push(noiseBuffer); } } this.poolInitialized = true; }, // Get a random pre-generated buffer from the pool getPooledBuffer(biome) { const pool = this.noiseBufferPool[biome]; if (!pool || pool.length === 0) return null; return pool[Math.floor(Math.random() * pool.length)]; }, // Play a single footstep playStep(isDash = false) { if (!AudioSystem.enabled || !AudioSystem.ctx) return; AudioSystem.resume(); const ctx = AudioSystem.ctx; const cfg = this.biomeConfig[this.currentBiome] || this.biomeConfig.Terra; const now = ctx.currentTime; // v7.24: Initialize buffer pool on first use (lazy init for faster startup) if (!this.poolInitialized) { this.initBufferPool(); } // v7.24: Use pooled buffer instead of creating new one each step const pooledBuffer = this.getPooledBuffer(this.currentBiome); const noiseSource = ctx.createBufferSource(); noiseSource.buffer = pooledBuffer; // Tone component for body const osc = ctx.createOscillator(); osc.type = cfg.type === 'metal' ? 'triangle' : 'sine'; // Add slight random variation to frequency const freqVariation = 1 + (Math.random() - 0.5) * 0.15; osc.frequency.value = cfg.baseFreq * freqVariation * (isDash ? 1.2 : 1); // Filter const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = cfg.filterFreq; // Gain envelope const gain = ctx.createGain(); const vol = AudioSystem.masterVolume * cfg.volume * (isDash ? 1.3 : 1); gain.gain.setValueAtTime(vol, now); gain.gain.exponentialRampToValueAtTime(0.001, now + cfg.duration); // Connect osc.connect(filter); noiseSource.connect(filter); filter.connect(gain); gain.connect(ctx.destination); // Play osc.start(now); noiseSource.start(now); osc.stop(now + cfg.duration); noiseSource.stop(now + cfg.duration); }, // Update footsteps based on player movement update(playerVelocity, isDashing) { if (!this.enabled) return; const now = performance.now(); const speed = playerVelocity ? Math.sqrt( playerVelocity.x * playerVelocity.x + playerVelocity.z * playerVelocity.z ) : 0; // Determine if moving const wasMoving = this.isMoving; this.isMoving = speed > 0.1; // Calculate step interval based on speed const interval = isDashing ? 200 : Math.max(250, this.stepInterval - speed * 50); // Play footstep if moving and interval elapsed if (this.isMoving && now - this.lastStep > interval) { this.playStep(isDashing); this.lastStep = now; } }, // Set current biome for footstep sounds setBiome(biome) { this.currentBiome = biome || 'Terra'; } }, // ═══════════════════════════════════════════════════════════════ // v6.32: ADAPTIVE COMBAT MUSIC SYSTEM // 8-Agent Consensus Implementation // Dynamic music intensity that responds to combat state // Uses pentatonic scale for always-harmonious layering // ═══════════════════════════════════════════════════════════════ combatMusic: { active: false, intensity: 0, // 0-5 scale targetIntensity: 0, nodes: [], updateInterval: null, lastIntensityChange: 0, decayDelay: 3000, // ms before intensity decays recentHits: 0, recentKills: 0, bossActive: false }, // Combat intensity configuration per level combatMusicConfig: { // Level 0: Silence (handled by ambient) // Level 1: Tension drone - single low note 1: { baseNote: 'C3', layers: 1, rhythm: false, tempo: 0 }, // Level 2: Building tension - two-note drone with subtle pulse 2: { baseNote: 'C3', layers: 2, rhythm: true, tempo: 40 }, // Level 3: Combat engaged - three layers with rhythm 3: { baseNote: 'G3', layers: 3, rhythm: true, tempo: 60 }, // Level 4: Intense combat - full layers, faster rhythm 4: { baseNote: 'C4', layers: 4, rhythm: true, tempo: 90 }, // Level 5: Boss battle - maximum intensity 5: { baseNote: 'C4', layers: 5, rhythm: true, tempo: 120 } }, // Start combat music system startCombatMusic() { if (!this.enabled || !this.ctx || this.combatMusic.active) return; this.resume(); this.combatMusic.active = true; this.combatMusic.intensity = 0; this.combatMusic.targetIntensity = 0; // v12.10: Notify SpaceMusic of combat state for ambient adaptation if (typeof SpaceMusic !== 'undefined') { SpaceMusic.setCombatState(true, 0); SpaceMusic.playTension(); // Subtle tension accent } // v7.37: Update intensity smoothly over time (migrated to TimerRegistry - Cycle 16 Code Quality) TimerRegistry.setInterval('combat-music-intensity', () => { this.updateCombatMusicIntensity(); // v12.10: Keep SpaceMusic updated on combat intensity if (typeof SpaceMusic !== 'undefined' && SpaceMusic.isPlaying) { SpaceMusic.tensionLevel = this.combatMusic.intensity / 5; } }, 100); }, stopCombatMusic() { if (!this.combatMusic.active) return; this.combatMusic.active = false; // v7.37: Use TimerRegistry for centralized timer management TimerRegistry.clearInterval('combat-music-intensity'); // v12.10: Notify SpaceMusic combat ended if (typeof SpaceMusic !== 'undefined') { SpaceMusic.setCombatState(false, 0); SpaceMusic.playResolve(); // Peaceful resolution accent } // Fade out all combat music nodes this.fadeCombatMusicOut(); }, // Register combat events to affect intensity combatEvent(type) { if (!this.combatMusic.active) return; const now = performance.now(); this.combatMusic.lastIntensityChange = now; switch(type) { case 'hit': this.combatMusic.recentHits++; this.combatMusic.targetIntensity = Math.min(5, this.combatMusic.targetIntensity + 0.3); break; case 'kill': this.combatMusic.recentKills++; this.combatMusic.targetIntensity = Math.min(5, this.combatMusic.targetIntensity + 0.5); // Brief intensity spike on kill this.playCombatAccent(); break; case 'crit': this.combatMusic.targetIntensity = Math.min(5, this.combatMusic.targetIntensity + 0.7); this.playCombatAccent(); break; case 'finisher': this.combatMusic.targetIntensity = Math.min(5, this.combatMusic.targetIntensity + 1.0); this.playCombatFinisherAccent(); break; case 'bossEngage': this.combatMusic.bossActive = true; this.combatMusic.targetIntensity = 5; break; case 'bossDefeat': this.combatMusic.bossActive = false; this.playCombatVictoryFanfare(); break; case 'damage': this.combatMusic.targetIntensity = Math.min(5, this.combatMusic.targetIntensity + 0.4); break; case 'nearEnemies': // Called when enemies are nearby this.combatMusic.targetIntensity = Math.max(1, this.combatMusic.targetIntensity); break; } }, updateCombatMusicIntensity() { if (!this.combatMusic.active || !this.ctx) return; const now = performance.now(); const timeSinceAction = now - this.combatMusic.lastIntensityChange; // Decay intensity over time when not in combat if (timeSinceAction > this.combatMusic.decayDelay && !this.combatMusic.bossActive) { this.combatMusic.targetIntensity = Math.max(0, this.combatMusic.targetIntensity - 0.05); } // Decay recent counters if (this.combatMusic.recentHits > 0) this.combatMusic.recentHits *= 0.95; if (this.combatMusic.recentKills > 0) this.combatMusic.recentKills *= 0.9; // Smooth intensity transition const diff = this.combatMusic.targetIntensity - this.combatMusic.intensity; if (Math.abs(diff) > 0.1) { this.combatMusic.intensity += diff * 0.1; // Update music layers when crossing intensity thresholds const newLevel = Math.floor(this.combatMusic.intensity); const currentLevel = Math.floor(this.combatMusic.intensity - diff * 0.1); if (newLevel !== currentLevel) { this.setCombatMusicLevel(newLevel); } } }, setCombatMusicLevel(level) { if (!this.ctx || level < 0 || level > 5) return; // Clear existing combat music nodes this.fadeCombatMusicOut(); if (level === 0) return; // Silence const config = this.combatMusicConfig[level]; if (!config) return; const baseFreq = this.penta[config.baseNote]; // Create layered drones for (let i = 0; i < config.layers; i++) { this.createCombatMusicLayer(baseFreq, i, config); } // Add rhythmic pulse if enabled if (config.rhythm && config.tempo > 0) { this.startCombatRhythm(config.tempo, level); } }, createCombatMusicLayer(baseFreq, layerIndex, config) { if (!this.ctx) return; const harmonics = [1, 1.5, 2, 2.5, 3]; const freq = baseFreq * harmonics[layerIndex % harmonics.length]; const vol = 0.025 * (1 - layerIndex * 0.15) * this.masterVolume; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); osc.type = layerIndex === 0 ? 'sine' : 'triangle'; osc.frequency.value = freq; filter.type = 'lowpass'; filter.frequency.value = 400 + layerIndex * 100; // Fade in gain.gain.setValueAtTime(0, this.ctx.currentTime); gain.gain.linearRampToValueAtTime(vol, this.ctx.currentTime + 0.5); // Add subtle LFO for movement const lfo = this.ctx.createOscillator(); const lfoGain = this.ctx.createGain(); lfo.frequency.value = 0.1 + layerIndex * 0.05; lfoGain.gain.value = freq * 0.02; lfo.connect(lfoGain).connect(osc.frequency); lfo.start(); osc.connect(filter).connect(gain).connect(this.ctx.destination); osc.start(); this.combatMusic.nodes.push({ osc, gain, lfo, filter, type: 'layer' }); }, // v7.39: combatRhythmInterval removed - migrated to TimerRegistry (Cycle 18 Code Quality) combatRhythmBeatCount: 0, combatRhythmLevel: 1, combatRhythmBeatMs: 500, startCombatRhythm(tempo, level) { // v7.39: Use TimerRegistry for centralized timer management (Cycle 18 Code Quality) TimerRegistry.clearInterval('combat-rhythm'); const beatMs = 60000 / tempo; this.combatRhythmBeatCount = 0; this.combatRhythmLevel = level; this.combatRhythmBeatMs = beatMs; TimerRegistry.setInterval('combat-rhythm', () => { if (!this.combatMusic.active) { TimerRegistry.clearInterval('combat-rhythm'); return; } // Play rhythmic pulse const pulseFreq = this.combatRhythmLevel >= 4 ? this.penta.G3 : this.penta.C3; const vol = 0.03 + (this.combatRhythmLevel * 0.01); // Accent on beat 1 const isAccent = this.combatRhythmBeatCount % 4 === 0; this.playGentle(pulseFreq, isAccent ? 0.15 : 0.08, isAccent ? vol * 1.3 : vol); // Higher intensity adds off-beat hits if (this.combatRhythmLevel >= 3 && this.combatRhythmBeatCount % 2 === 1) { setTimeout(() => { this.playGentle(this.penta.E3, 0.06, vol * 0.5); }, this.combatRhythmBeatMs / 2); } this.combatRhythmBeatCount++; }, beatMs); }, fadeCombatMusicOut() { // v7.39: Use TimerRegistry for centralized timer management (Cycle 18 Code Quality) TimerRegistry.clearInterval('combat-rhythm'); // Fade out and cleanup all nodes this.combatMusic.nodes.forEach(node => { try { if (node.gain && this.ctx) { node.gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.5); } setTimeout(() => { try { if (node.osc) { node.osc.stop(); node.osc.disconnect(); } if (node.lfo) { node.lfo.stop(); node.lfo.disconnect(); } if (node.filter) { node.filter.disconnect(); } } catch(e) {} }, 600); } catch(e) {} }); this.combatMusic.nodes = []; }, // Combat accents for dramatic moments playCombatAccent() { if (!this.enabled || !this.ctx) return; this.playGentle(this.penta.G4, 0.12, 0.15); setTimeout(() => this.playGentle(this.penta.C5, 0.1, 0.08), 40); }, playCombatFinisherAccent() { if (!this.enabled || !this.ctx) return; [this.penta.C4, this.penta.E4, this.penta.G4].forEach((f, i) => { setTimeout(() => this.playGentle(f, 0.2, 0.2 - i * 0.04), i * 50); }); }, playCombatVictoryFanfare() { if (!this.enabled || !this.ctx) return; [this.penta.G4, this.penta.C5, this.penta.E5, this.penta.G5].forEach((f, i) => { setTimeout(() => this.playGentle(f, 0.4, 0.25 - i * 0.03), i * 120); }); } }; // Initialize nature soundscape integration AudioSystem.initNature(); // ═══════════════════════════════════════════════════════════════════════════════════════ // v7.32: 3D SPATIAL AUDIO SYSTEM (8-Strategy Consensus Cycle 5 - 3/8 agents) // True HRTF-based 3D audio positioning for immersive sound // ═══════════════════════════════════════════════════════════════════════════════════════ const SpatialAudioSystem = { ctx: null, listener: null, activeSounds: [], maxActiveSounds: 12, // Polyphony limit tempVec: new THREE.Vector3(), // v8.0: Pre-allocated vectors for updateListener() (8-Strategy Consensus Cycle 5) // Eliminates per-frame GC pressure from new THREE.Vector3() calls _forward: new THREE.Vector3(), _up: new THREE.Vector3(), init() { this.ctx = getSharedAudioContext(); if (!this.ctx) return; this.listener = this.ctx.listener; }, // Update listener position from camera/player updateListener(camera) { if (!this.listener || !camera) return; // Get camera world position camera.getWorldPosition(this.tempVec); const pos = this.tempVec; // v8.0: Use pre-allocated vectors instead of new THREE.Vector3() per frame // Get camera forward direction const forward = this._forward.set(0, 0, -1); forward.applyQuaternion(camera.quaternion); const up = this._up.set(0, 1, 0); up.applyQuaternion(camera.quaternion); // Set listener position if (this.listener.positionX) { this.listener.positionX.value = pos.x; this.listener.positionY.value = pos.y; this.listener.positionZ.value = pos.z; this.listener.forwardX.value = forward.x; this.listener.forwardY.value = forward.y; this.listener.forwardZ.value = forward.z; this.listener.upX.value = up.x; this.listener.upY.value = up.y; this.listener.upZ.value = up.z; } else { // Fallback for older browsers this.listener.setPosition(pos.x, pos.y, pos.z); this.listener.setOrientation(forward.x, forward.y, forward.z, up.x, up.y, up.z); } }, // Play a 3D positioned sound play3D(soundConfig, worldPosition) { if (!this.ctx || !AudioSystem.enabled) return; if (this.activeSounds.length >= this.maxActiveSounds) return; const panner = this.ctx.createPanner(); panner.panningModel = 'HRTF'; panner.distanceModel = 'inverse'; panner.refDistance = 5; panner.maxDistance = 80; panner.rolloffFactor = 1; panner.coneInnerAngle = 360; panner.coneOuterAngle = 0; panner.coneOuterGain = 0; // Set panner position if (panner.positionX) { panner.positionX.value = worldPosition.x; panner.positionY.value = worldPosition.y; panner.positionZ.value = worldPosition.z; } else { panner.setPosition(worldPosition.x, worldPosition.y, worldPosition.z); } // Create oscillator for the sound const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); osc.type = soundConfig.type || 'sine'; osc.frequency.value = soundConfig.freq || 440; filter.type = 'lowpass'; filter.frequency.value = soundConfig.filterFreq || 2000; const now = this.ctx.currentTime; const dur = soundConfig.dur || 0.3; const vol = (soundConfig.vol || 0.3) * AudioSystem.masterVolume; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(vol, now + 0.02); gain.gain.exponentialRampToValueAtTime(0.001, now + dur); osc.connect(filter).connect(gain).connect(panner).connect(this.ctx.destination); osc.start(now); osc.stop(now + dur + 0.05); // Track for cleanup const soundEntry = { panner, endTime: now + dur + 0.1 }; this.activeSounds.push(soundEntry); return panner; }, // Play enemy hit sound at position playHit3D(position, damage = 10) { const baseFreq = 200 + Math.min(damage * 5, 150); this.play3D({ type: 'sawtooth', freq: baseFreq, dur: 0.15, vol: 0.2, filterFreq: 1500 }, position); }, // Play enemy death sound at position playDeath3D(position) { this.play3D({ type: 'sine', freq: 150, dur: 0.4, vol: 0.25, filterFreq: 800 }, position); // Harmonic setTimeout(() => { this.play3D({ type: 'sine', freq: 225, dur: 0.3, vol: 0.15 }, position); }, 50); }, // Play collect sound at position playCollect3D(position) { this.play3D({ type: 'sine', freq: 660, dur: 0.2, vol: 0.15 }, position); }, // v7.30: Play enemy aggro alert sound at position (8-Strategy Cycle 9 Consensus) // Directional audio cue when enemy becomes aggressive - warns player of incoming threat playAggro3D(position) { // Low threatening growl tone this.play3D({ type: 'sawtooth', freq: 120, // Low rumble dur: 0.25, vol: 0.18, filterFreq: 600 }, position); // Higher alert ping for directionality setTimeout(() => { this.play3D({ type: 'triangle', freq: 300, dur: 0.12, vol: 0.12 }, position); }, 80); }, // v7.31: Play enemy attack telegraph sound at position (8-Strategy Cycle 10 Consensus) // Spatial warning tone when enemy winds up an attack - directional awareness // windupMs: attack windup duration to scale urgency // damageLevel: 'light' | 'medium' | 'heavy' | 'lethal' to scale intensity playTelegraph3D(position, windupMs = 800, damageLevel = 'medium') { if (!this.ctx || !position) return; // Base frequency and volume scale with damage level const levels = { light: { baseFreq: 400, vol: 0.08, riseAmount: 100 }, medium: { baseFreq: 300, vol: 0.12, riseAmount: 150 }, heavy: { baseFreq: 200, vol: 0.16, riseAmount: 200 }, lethal: { baseFreq: 150, vol: 0.20, riseAmount: 250 } }; const level = levels[damageLevel] || levels.medium; // Initial warning ping - directional awareness this.play3D({ type: 'triangle', freq: level.baseFreq, dur: 0.15, vol: level.vol * 0.6 }, position); // Rising tension tone during windup const riseDuration = Math.min(windupMs * 0.7, 600) / 1000; setTimeout(() => { // Rising tone that crescendos to attack moment this.play3D({ type: 'sawtooth', freq: level.baseFreq + level.riseAmount * 0.5, dur: riseDuration, vol: level.vol * 0.8, filterFreq: 800 }, position); }, 80); // Final warning ping just before attack lands const finalDelay = Math.max(windupMs - 150, 200); setTimeout(() => { this.play3D({ type: 'square', freq: level.baseFreq + level.riseAmount, dur: 0.08, vol: level.vol }, position); }, finalDelay); }, // v7.32: Play parry sound at position (8-Strategy Cycle 11 Consensus) // Distinct metallic deflect sound with spatial positioning playParry3D(position) { if (!this.ctx || !position) return; // Metallic clang - high frequency burst this.play3D({ type: 'square', freq: 900, dur: 0.08, vol: 0.2, filterFreq: 3000 }, position); // Detuned shimmer layer setTimeout(() => { this.play3D({ type: 'square', freq: 950, dur: 0.1, vol: 0.15, filterFreq: 2500 }, position); }, 10); // Rising power tone setTimeout(() => { this.play3D({ type: 'triangle', freq: 440, dur: 0.2, vol: 0.12 }, position); }, 80); }, // v7.35: Play incoming damage sound at attacker position (Cycle 14 - Audio/Feedback) // HRTF spatial audio tells player WHERE they were hit from // Complements visual flashDirectionalDamage() with audio directionality playDamageReceived3D(attackerPosition, damageAmount = 10) { if (!this.ctx || !AudioSystem.enabled || !attackerPosition) return; // Scale intensity with damage (capped) const intensity = Math.min(damageAmount / 50, 1.0); // Low impact thump - painful but not annoying this.play3D({ type: 'sine', freq: 80 + intensity * 40, // 80-120 Hz (sub-bass impact) dur: 0.12 + intensity * 0.08, vol: 0.2 + intensity * 0.1, filterFreq: 400 }, attackerPosition); // Higher alert component for directional localization setTimeout(() => { this.play3D({ type: 'triangle', freq: 200 + intensity * 100, // 200-300 Hz dur: 0.08, vol: 0.12 + intensity * 0.06 }, attackerPosition); }, 20); }, // Cleanup expired sounds update() { if (!this.ctx) return; const now = this.ctx.currentTime; this.activeSounds = this.activeSounds.filter(s => s.endTime > now); } }; // ═══════════════════════════════════════════════════════════════════════════════════════ // v12.10: ADAPTIVE SPACE AMBIENT MUSIC SYSTEM // The core musical identity of LEVIATHAN: OMNIVERSE // Procedural, lo-fi space ambient that adapts to game context // - Mode-aware: Galaxy (cosmic), World (grounded), Tesseract (ethereal) // - Biome-influenced: Subtle tonal shifts based on planet environment // - Combat-aware: Reduces melodic activity during tension, maintains atmosphere // - Moment-reactive: Special accents for discoveries, landings, achievements // ═══════════════════════════════════════════════════════════════════════════════════════ const SpaceMusic = { ctx: null, isPlaying: false, masterGain: null, volume: 0.15, nodes: [], melodyTimeout: null, chordTimeout: null, arpTimeout: null, shimmerTimeout: null, // Current game context for adaptive music currentMode: 'galaxy', // galaxy, world, tesseract currentBiome: 'Terra', // Affects tonal color inCombat: false, // Reduces melodic activity tensionLevel: 0, // 0-1, affects music intensity // Pentatonic scales for different moods (all harmonious) scales: { // Default cosmic scale (A minor pentatonic) cosmic: [220, 261.63, 293.66, 329.63, 392, 440, 523.25, 587.33, 659.25, 783.99], // Warmer scale for Terra/forest biomes (C major pentatonic) warm: [261.63, 293.66, 329.63, 392, 440, 523.25, 587.33, 659.25, 783.99, 880], // Cooler scale for Ice/space (E minor pentatonic) cool: [164.81, 196, 220, 246.94, 293.66, 329.63, 392, 440, 493.88, 587.33], // Mysterious scale for Alien/Volcanic (D minor pentatonic) mysterious: [146.83, 174.61, 196, 220, 261.63, 293.66, 349.23, 392, 440, 523.25], // Ethereal scale for Tesseract (whole tone based) ethereal: [220, 246.94, 277.18, 311.13, 349.23, 392, 440, 493.88, 554.37, 622.25] }, // Biome to scale mapping biomeScales: { Terra: 'warm', Desert: 'warm', Ice: 'cool', Volcanic: 'mysterious', Alien: 'mysterious', Magma: 'mysterious', Ocean: 'cool', Forest: 'warm', Crystal: 'ethereal' }, // Mode-specific pad configurations modeConfigs: { galaxy: { padNotes: [55, 82.41, 110], // Very low, cosmic filterBase: 300, melodyOctave: 1, melodyDensity: 0.6, shimmer: true, cosmicWashVol: 0.04 }, world: { padNotes: [110, 165, 220], // Warmer, more present filterBase: 500, melodyOctave: 1, melodyDensity: 0.8, shimmer: false, cosmicWashVol: 0.02 }, tesseract: { padNotes: [73.42, 110, 146.83], // Unsettling intervals filterBase: 600, melodyOctave: 2, melodyDensity: 0.4, shimmer: true, cosmicWashVol: 0.05 } }, // Chord roots for progression chordRoots: [220, 174.61, 261.63, 196], currentChordIndex: 0, init() { if (this.ctx) return; try { this.ctx = AudioSystem.ctx || new (window.AudioContext || window.webkitAudioContext)(); this.masterGain = this.ctx.createGain(); this.masterGain.gain.value = 0; // Start silent for fade-in this.masterGain.connect(this.ctx.destination); } catch(e) { Logger.warn('SpaceMusic', 'Could not initialize audio context'); } }, resume() { if (this.ctx && this.ctx.state === 'suspended') { this.ctx.resume(); } }, // Get current scale based on mode and biome getCurrentScale() { if (this.currentMode === 'tesseract') return this.scales.ethereal; if (this.currentMode === 'galaxy') return this.scales.cosmic; const scaleName = this.biomeScales[this.currentBiome] || 'cosmic'; return this.scales[scaleName]; }, // Get current mode config getModeConfig() { return this.modeConfigs[this.currentMode] || this.modeConfigs.galaxy; }, // Update game context (called by game state changes) setMode(newMode) { if (this.currentMode === newMode) return; const oldMode = this.currentMode; this.currentMode = newMode; if (this.isPlaying) { // Crossfade to new mode settings this.transitionToMode(oldMode, newMode); } debugLog('Music', `Adapting to ${newMode} mode`); // v8.25: gated }, setBiome(biomeName) { if (this.currentBiome === biomeName) return; this.currentBiome = biomeName; debugLog('Music', `Tinted by ${biomeName} atmosphere`); // v8.25: gated }, setCombatState(inCombat, tensionLevel = 0) { this.inCombat = inCombat; this.tensionLevel = Math.max(0, Math.min(1, tensionLevel)); // Adjust melody density based on combat if (this.isPlaying && this.melodyTimeout) { // Clear and restart melody with new density clearTimeout(this.melodyTimeout); this.startMelody(); } }, // Smooth transition between modes transitionToMode(oldMode, newMode) { const config = this.getModeConfig(); const now = this.ctx.currentTime; // Adjust cosmic wash volume this.nodes.forEach(node => { if (node.isCosmicWash && node.gain) { node.gain.gain.linearRampToValueAtTime(config.cosmicWashVol, now + 2); } }); }, // Start the ambient space music start() { if (this.isPlaying) return; this.init(); if (!this.ctx) return; this.resume(); this.isPlaying = true; // Start the layers this.startPad(); this.startMelody(); this.startArpeggio(); this.startCosmicWash(); // v12.10: Start shimmer for galaxy/tesseract modes const config = this.getModeConfig(); if (config.shimmer) { this.startShimmer(); } // Gentle fade-in const now = this.ctx.currentTime; this.masterGain.gain.setValueAtTime(0, now); this.masterGain.gain.linearRampToValueAtTime(this.volume, now + 4); debugLog('Music', `Space ambient started (${this.currentMode} mode)`); // v8.25: gated }, stop() { if (!this.isPlaying) return; this.isPlaying = false; // Clear timeouts if (this.melodyTimeout) clearTimeout(this.melodyTimeout); if (this.chordTimeout) clearTimeout(this.chordTimeout); if (this.arpTimeout) clearTimeout(this.arpTimeout); if (this.shimmerTimeout) clearTimeout(this.shimmerTimeout); // v7.50: Clear pending cleanup timeouts via TimerRegistry (Cycle 29 Code Quality) TimerRegistry.clearTimeout('space-music-node-cleanup'); TimerRegistry.clearTimeout('space-music-array-cleanup'); // Fade out and stop all nodes const now = this.ctx.currentTime; this.masterGain.gain.linearRampToValueAtTime(0, now + 2); // v7.50: Use TimerRegistry for node cleanup timeout (Cycle 29 Code Quality) const nodesToClean = [...this.nodes]; TimerRegistry.setTimeout('space-music-node-cleanup', () => { nodesToClean.forEach(node => { try { if (node.osc) node.osc.stop(); if (node.noise) node.noise.stop(); } catch(e) {} }); }, 2100); // v7.50: Use TimerRegistry for array cleanup timeout (Cycle 29 Code Quality) TimerRegistry.setTimeout('space-music-array-cleanup', () => { this.nodes = []; }, 2200); Logger.debug('SpaceMusic', 'Space ambient music stopped'); }, toggle() { if (this.isPlaying) { this.stop(); } else { this.start(); } return this.isPlaying; }, setVolume(vol) { this.volume = Math.max(0, Math.min(1, vol)); if (this.masterGain) { this.masterGain.gain.linearRampToValueAtTime(this.volume, this.ctx.currentTime + 0.1); } }, // Warm pad drone - the foundation startPad() { const now = this.ctx.currentTime; // Create a warm, evolving pad with multiple detuned oscillators const padNotes = [110, 165, 220]; // Root, fifth, octave padNotes.forEach((freq, i) => { const osc1 = this.ctx.createOscillator(); const osc2 = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); // Slightly detuned for warmth osc1.type = 'sine'; osc1.frequency.value = freq; osc2.type = 'triangle'; osc2.frequency.value = freq * 1.002; // Slight detune // LFO for gentle movement const lfo = this.ctx.createOscillator(); const lfoGain = this.ctx.createGain(); lfo.type = 'sine'; lfo.frequency.value = 0.05 + i * 0.02; lfoGain.gain.value = freq * 0.01; lfo.connect(lfoGain); lfoGain.connect(osc1.frequency); lfoGain.connect(osc2.frequency); // Warm low-pass filter filter.type = 'lowpass'; filter.frequency.value = 400 + i * 100; filter.Q.value = 0.5; // Filter modulation for evolving texture const filterLfo = this.ctx.createOscillator(); const filterLfoGain = this.ctx.createGain(); filterLfo.type = 'sine'; filterLfo.frequency.value = 0.02; filterLfoGain.gain.value = 150; filterLfo.connect(filterLfoGain); filterLfoGain.connect(filter.frequency); gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.08 - i * 0.02, now + 4); osc1.connect(filter); osc2.connect(filter); filter.connect(gain); gain.connect(this.masterGain); osc1.start(now); osc2.start(now); lfo.start(now); filterLfo.start(now); this.nodes.push({ osc: osc1, gain }, { osc: osc2 }, { osc: lfo }, { osc: filterLfo }); }); // Schedule chord changes this.scheduleChordChange(); }, scheduleChordChange() { if (!this.isPlaying) return; this.chordTimeout = setTimeout(() => { this.currentChordIndex = (this.currentChordIndex + 1) % this.chordRoots.length; // Update pad frequencies smoothly (would need to rebuild for true changes) this.scheduleChordChange(); }, 8000 + Math.random() * 4000); }, // v12.10: Context-aware gentle melody - sparse, contemplative notes startMelody() { if (!this.isPlaying) return; const playNote = () => { if (!this.isPlaying) return; const config = this.getModeConfig(); const scale = this.getCurrentScale(); // v12.10: Skip note if in combat (reduce melodic density) if (this.inCombat && Math.random() > 0.3) { // Schedule next check sooner during combat this.melodyTimeout = setTimeout(playNote, 2000 + Math.random() * 2000); return; } // v12.10: Check melody density based on mode if (Math.random() > config.melodyDensity) { this.melodyTimeout = setTimeout(playNote, 2000 + Math.random() * 3000); return; } // Pick a random note from current scale, weighted toward middle range const noteIndex = Math.floor(Math.pow(Math.random(), 0.7) * scale.length); const freq = scale[noteIndex] * config.melodyOctave; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); osc.type = 'sine'; osc.frequency.value = freq; filter.type = 'lowpass'; filter.frequency.value = config.filterBase + 1500; const now = this.ctx.currentTime; const duration = 2 + Math.random() * 3; // Soft attack, long decay - quieter during combat const vol = this.inCombat ? 0.06 : 0.12; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(vol, now + 0.3); gain.gain.exponentialRampToValueAtTime(0.001, now + duration); osc.connect(filter); filter.connect(gain); gain.connect(this.masterGain); osc.start(now); osc.stop(now + duration + 0.1); // Schedule next note (sparse - every 3-8 seconds, faster during non-combat) const baseDelay = this.inCombat ? 4000 : 3000; const variance = this.inCombat ? 4000 : 5000; this.melodyTimeout = setTimeout(playNote, baseDelay + Math.random() * variance); }; // Start after a brief delay this.melodyTimeout = setTimeout(playNote, 2000); }, // Soft arpeggio - occasional rippling notes startArpeggio() { if (!this.isPlaying) return; const playArp = () => { if (!this.isPlaying) return; // Only play arpeggio sometimes if (Math.random() > 0.4) { this.arpTimeout = setTimeout(playArp, 4000 + Math.random() * 6000); return; } const baseIndex = Math.floor(Math.random() * 4); const notes = [ this.scale[baseIndex], this.scale[baseIndex + 2], this.scale[baseIndex + 4], this.scale[baseIndex + 2] ]; notes.forEach((freq, i) => { setTimeout(() => { if (!this.isPlaying) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq * 2; // Octave up const now = this.ctx.currentTime; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.06, now + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, now + 1.5); osc.connect(gain); gain.connect(this.masterGain); osc.start(now); osc.stop(now + 1.6); }, i * 200); }); this.arpTimeout = setTimeout(playArp, 8000 + Math.random() * 8000); }; setTimeout(playArp, 5000); }, // Cosmic wash - subtle noise texture startCosmicWash() { const bufferSize = this.ctx.sampleRate * 2; const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); const data = buffer.getChannelData(0); // Pink-ish noise let b0 = 0, b1 = 0, b2 = 0; for (let i = 0; i < bufferSize; i++) { const white = Math.random() * 2 - 1; b0 = 0.99765 * b0 + white * 0.0990460; b1 = 0.96300 * b1 + white * 0.2965164; b2 = 0.57000 * b2 + white * 1.0526913; data[i] = (b0 + b1 + b2 + white * 0.1848) * 0.11; } const noise = this.ctx.createBufferSource(); noise.buffer = buffer; noise.loop = true; const filter = this.ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 300; const gain = this.ctx.createGain(); gain.gain.value = 0.03; // Gentle modulation const lfo = this.ctx.createOscillator(); const lfoGain = this.ctx.createGain(); lfo.frequency.value = 0.03; lfoGain.gain.value = 100; lfo.connect(lfoGain); lfoGain.connect(filter.frequency); noise.connect(filter); filter.connect(gain); gain.connect(this.masterGain); noise.start(); lfo.start(); this.nodes.push({ noise, gain, isCosmicWash: true }, { osc: lfo }); }, // v12.10: Shimmer layer - high frequency sparkles for galaxy/tesseract startShimmer() { if (!this.isPlaying) return; const playShimmer = () => { if (!this.isPlaying) return; const config = this.getModeConfig(); if (!config.shimmer) { this.shimmerTimeout = setTimeout(playShimmer, 5000); return; } // Random high note const scale = this.getCurrentScale(); const freq = scale[Math.floor(Math.random() * scale.length)] * 4; // Two octaves up const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); const filter = this.ctx.createBiquadFilter(); osc.type = 'sine'; osc.frequency.value = freq; filter.type = 'highpass'; filter.frequency.value = 1000; const now = this.ctx.currentTime; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.03, now + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, now + 2); osc.connect(filter); filter.connect(gain); gain.connect(this.masterGain); osc.start(now); osc.stop(now + 2.1); // Random interval this.shimmerTimeout = setTimeout(playShimmer, 1000 + Math.random() * 4000); }; setTimeout(playShimmer, 3000); }, // ═══════════════════════════════════════════════════════════════ // v12.10: SPECIAL MOMENT MUSICAL ACCENTS // These can be called from anywhere in the game for key moments // ═══════════════════════════════════════════════════════════════ // Discovery sound - ascending sparkle (finding new things) playDiscovery() { if (!this.ctx || !this.isPlaying) return; const scale = this.getCurrentScale(); const now = this.ctx.currentTime; [0, 2, 4, 6].forEach((idx, i) => { setTimeout(() => { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.value = scale[idx % scale.length] * 2; gain.gain.setValueAtTime(0, this.ctx.currentTime); gain.gain.linearRampToValueAtTime(0.15, this.ctx.currentTime + 0.02); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.8); osc.connect(gain); gain.connect(this.masterGain); osc.start(); osc.stop(this.ctx.currentTime + 0.9); }, i * 100); }); }, // Landing sound - grounding, arrival (entering a planet) playLanding() { if (!this.ctx || !this.isPlaying) return; const now = this.ctx.currentTime; // Deep resonant tone const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(110, now); osc.frequency.linearRampToValueAtTime(55, now + 2); gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.2, now + 0.5); gain.gain.exponentialRampToValueAtTime(0.001, now + 3); osc.connect(gain); gain.connect(this.masterGain); osc.start(now); osc.stop(now + 3.1); // Harmonic overtone const osc2 = this.ctx.createOscillator(); const gain2 = this.ctx.createGain(); osc2.type = 'sine'; osc2.frequency.value = 165; gain2.gain.setValueAtTime(0, now); gain2.gain.linearRampToValueAtTime(0.08, now + 0.8); gain2.gain.exponentialRampToValueAtTime(0.001, now + 2.5); osc2.connect(gain2); gain2.connect(this.masterGain); osc2.start(now + 0.3); osc2.stop(now + 2.6); }, // Departure/launch sound - ascending, hopeful playLaunch() { if (!this.ctx || !this.isPlaying) return; const scale = this.getCurrentScale(); const now = this.ctx.currentTime; // Rising sweep [0, 2, 4, 7, 9].forEach((idx, i) => { setTimeout(() => { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.value = scale[idx % scale.length]; gain.gain.setValueAtTime(0, this.ctx.currentTime); gain.gain.linearRampToValueAtTime(0.1, this.ctx.currentTime + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 1.5); osc.connect(gain); gain.connect(this.masterGain); osc.start(); osc.stop(this.ctx.currentTime + 1.6); }, i * 150); }); }, // Achievement/milestone sound - triumphant chord playAchievement() { if (!this.ctx || !this.isPlaying) return; const now = this.ctx.currentTime; // Major chord: root, third, fifth, octave [220, 277.18, 329.63, 440].forEach((freq, i) => { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; const delay = i * 0.05; gain.gain.setValueAtTime(0, now + delay); gain.gain.linearRampToValueAtTime(0.12 - i * 0.02, now + delay + 0.1); gain.gain.exponentialRampToValueAtTime(0.001, now + delay + 2); osc.connect(gain); gain.connect(this.masterGain); osc.start(now + delay); osc.stop(now + delay + 2.1); }); }, // Tension/danger sound - dissonant undertone playTension() { if (!this.ctx || !this.isPlaying) return; const now = this.ctx.currentTime; // Tritone interval - creates unease [110, 155.56].forEach((freq, i) => { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.value = freq; const filter = this.ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 400; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.04, now + 0.5); gain.gain.linearRampToValueAtTime(0.04, now + 2); gain.gain.exponentialRampToValueAtTime(0.001, now + 3); osc.connect(filter); filter.connect(gain); gain.connect(this.masterGain); osc.start(now); osc.stop(now + 3.1); }); }, // Calm/peaceful resolution playResolve() { if (!this.ctx || !this.isPlaying) return; const now = this.ctx.currentTime; // Perfect fifth - resolution [220, 330, 440].forEach((freq, i) => { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.08, now + 0.3); gain.gain.exponentialRampToValueAtTime(0.001, now + 4); osc.connect(gain); gain.connect(this.masterGain); osc.start(now); osc.stop(now + 4.1); }); }, // v12.12: Cinematic landing sequence accent - atmospheric wonder playLandingAccent() { if (!this.ctx) return; this.init(); this.resume(); const now = this.ctx.currentTime; const dest = this.masterGain || this.ctx.destination; // Layer 1: Deep space drone with slow swell const drone = this.ctx.createOscillator(); const droneGain = this.ctx.createGain(); const droneFilter = this.ctx.createBiquadFilter(); drone.type = 'sine'; drone.frequency.value = 55; // Low A droneFilter.type = 'lowpass'; droneFilter.frequency.value = 200; droneGain.gain.setValueAtTime(0, now); droneGain.gain.linearRampToValueAtTime(0.12, now + 3); droneGain.gain.setValueAtTime(0.12, now + 8); droneGain.gain.exponentialRampToValueAtTime(0.001, now + 12); drone.connect(droneFilter); droneFilter.connect(droneGain); droneGain.connect(dest); drone.start(now); drone.stop(now + 12.1); // Layer 2: Ethereal pad (fifth above) const pad = this.ctx.createOscillator(); const padGain = this.ctx.createGain(); const padFilter = this.ctx.createBiquadFilter(); pad.type = 'sine'; pad.frequency.value = 82.41; // Low E (fifth) padFilter.type = 'lowpass'; padFilter.frequency.value = 300; padGain.gain.setValueAtTime(0, now + 1); padGain.gain.linearRampToValueAtTime(0.08, now + 4); padGain.gain.setValueAtTime(0.08, now + 7); padGain.gain.exponentialRampToValueAtTime(0.001, now + 11); pad.connect(padFilter); padFilter.connect(padGain); padGain.connect(dest); pad.start(now + 1); pad.stop(now + 11.1); // Layer 3: Shimmering high harmonics (arrival wonder) setTimeout(() => { if (!this.ctx) return; const shimmerFreqs = [440, 554.37, 659.25, 880]; shimmerFreqs.forEach((freq, i) => { setTimeout(() => { if (!this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; gain.gain.setValueAtTime(0, this.ctx.currentTime); gain.gain.linearRampToValueAtTime(0.04, this.ctx.currentTime + 0.5); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 3); osc.connect(gain); gain.connect(dest); osc.start(); osc.stop(this.ctx.currentTime + 3.1); }, i * 400); }); }, 2000); // Layer 4: Descending tone (spacecraft descent) const descent = this.ctx.createOscillator(); const descentGain = this.ctx.createGain(); descent.type = 'triangle'; descent.frequency.setValueAtTime(220, now + 0.5); descent.frequency.exponentialRampToValueAtTime(110, now + 6); descentGain.gain.setValueAtTime(0, now + 0.5); descentGain.gain.linearRampToValueAtTime(0.05, now + 1.5); descentGain.gain.exponentialRampToValueAtTime(0.001, now + 6); descent.connect(descentGain); descentGain.connect(dest); descent.start(now + 0.5); descent.stop(now + 6.1); // Layer 5: Wind/atmosphere texture const bufferSize = this.ctx.sampleRate * 8; const noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); const output = noiseBuffer.getChannelData(0); for (let i = 0; i < bufferSize; i++) { output[i] = (Math.random() * 2 - 1) * 0.5; } const noise = this.ctx.createBufferSource(); noise.buffer = noiseBuffer; const noiseGain = this.ctx.createGain(); const noiseFilter = this.ctx.createBiquadFilter(); noiseFilter.type = 'bandpass'; noiseFilter.frequency.value = 800; noiseFilter.Q.value = 0.5; noiseGain.gain.setValueAtTime(0, now + 2); noiseGain.gain.linearRampToValueAtTime(0.015, now + 4); noiseGain.gain.setValueAtTime(0.015, now + 6); noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 10); noise.connect(noiseFilter); noiseFilter.connect(noiseGain); noiseGain.connect(dest); noise.start(now + 2); noise.stop(now + 10.1); Logger.debug('SpaceMusic', 'Landing sequence ambient accent started'); } }; // Make SpaceMusic globally accessible window.SpaceMusic = SpaceMusic; // ═══════════════════════════════════════════════════════════════════════════════════════ // v12.11: ADAPTIVE VOLUME MUSIC CONTROLLER // Two-tier volume system: subtle during gameplay, fuller during idle/cinema // - Gameplay Mode: Very low volume (0.06) - adds atmosphere without competing // - Idle/Cinema Mode: Full volume (0.15) - immersive ambient experience // Music starts on first interaction at gameplay level, rises when idle // ═══════════════════════════════════════════════════════════════════════════════════════ const SpaceMusicController = { idleTimeout: null, idleThreshold: 15000, // 15 seconds to transition to idle volume gameplayStartDelay: 3000, // Start music 3 seconds after first interaction userDisabled: false, hasAutoStarted: false, lastActivity: Date.now(), isIdle: false, hasHadFirstInteraction: false, volumeTransitionInterval: null, // Volume tiers volumes: { gameplay: 0.06, // Very subtle - background atmosphere idle: 0.15, // Fuller - immersive ambient cinema: 0.18 // Slightly higher for second screen mode }, // Current target volume targetVolume: 0.06, // Show a subtle notification showNotification(msg, duration = 3000) { const notify = document.createElement('div'); notify.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, rgba(0,20,40,0.95) 0%, rgba(0,10,30,0.95) 100%); color: #0ff; padding: 12px 24px; border-radius: 12px; z-index: 9999; font-size: 14px; border: 1px solid rgba(0,255,255,0.4); pointer-events: none; box-shadow: 0 4px 20px rgba(0,255,255,0.2), inset 0 1px 0 rgba(255,255,255,0.1); backdrop-filter: blur(10px); animation: spaceMusicFadeIn 0.5s ease-out; `; notify.textContent = msg; document.body.appendChild(notify); // Fade out before removing setTimeout(() => { notify.style.transition = 'opacity 0.5s ease-out'; notify.style.opacity = '0'; setTimeout(() => notify.remove(), 500); }, duration - 500); }, // Reset the idle timer on any activity resetIdleTimer() { this.lastActivity = Date.now(); // Clear existing timeout if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; } // Don't set new timer if: // - Music is already playing (let it continue) // - User manually disabled music (respect their choice) if (SpaceMusic.isPlaying || this.userDisabled) { return; } // Set new idle timeout to auto-start music this.idleTimeout = setTimeout(() => { // Double-check conditions before starting if (!SpaceMusic.isPlaying && !this.userDisabled) { SpaceMusic.start(); this.hasAutoStarted = true; this.showNotification('🎵 Ambient space music activated... (Press M to toggle)', 4000); } }, this.idleThreshold); }, // Manual toggle - tracks user preference toggle() { const isPlaying = SpaceMusic.toggle(); if (isPlaying) { // User manually enabled - clear disabled flag this.userDisabled = false; this.showNotification('🎵 Space Music: ON'); } else { // User manually disabled - set flag to prevent auto-restart this.userDisabled = true; this.showNotification('🔇 Space Music: OFF (will not auto-start)'); // Clear any pending auto-start timeout if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; } } return isPlaying; }, // Initialize the controller init() { // Activity events to track (passive for performance) const activityEvents = [ 'mousemove', 'mousedown', 'mouseup', 'keydown', 'keyup', 'touchstart', 'touchmove', 'scroll', 'wheel', 'click', 'contextmenu' ]; // Throttle activity detection to avoid excessive calls let lastReset = 0; const throttleMs = 1000; // Only reset timer once per second max const handleActivity = () => { const now = Date.now(); if (now - lastReset > throttleMs) { lastReset = now; this.resetIdleTimer(); } }; // Attach listeners activityEvents.forEach(event => { document.addEventListener(event, handleActivity, { passive: true }); }); // v8.39: Track visibility changes via centralized manager PageVisibilityManager.subscribe('afkTracker', (isVisible) => { if (isVisible) { // Tab became visible - reset timer handleActivity(); } }); // v12.12: Start music on first click/touch (browser requires user interaction for audio) const startOnFirstInteraction = () => { if (!this.hasAutoStarted && !this.userDisabled) { // Small delay for smoother experience setTimeout(() => { if (!SpaceMusic.isPlaying && !this.userDisabled) { SpaceMusic.start(); this.hasAutoStarted = true; this.showNotification('🎵 Ambient space music... (M to toggle, [ ] for volume)', 4000); } }, 1500); } // Remove listener after first interaction document.removeEventListener('click', startOnFirstInteraction); document.removeEventListener('touchstart', startOnFirstInteraction); }; document.addEventListener('click', startOnFirstInteraction, { once: true }); document.addEventListener('touchstart', startOnFirstInteraction, { once: true }); // Start the initial idle timer as backup this.resetIdleTimer(); Logger.info('SpaceMusic', 'SpaceMusicController initialized - music starts on first interaction'); } }; // Initialize the controller SpaceMusicController.init(); window.SpaceMusicController = SpaceMusicController; // Add keyboard shortcuts document.addEventListener('keydown', (e) => { // Don't trigger when typing in inputs if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; // M key - toggle music if (e.key === 'm' || e.key === 'M') { SpaceMusicController.toggle(); } // [ key - decrease volume if (e.key === '[') { SpaceMusic.setVolume(SpaceMusic.volume - 0.05); SpaceMusicController.showNotification(`🔊 Volume: ${Math.round(SpaceMusic.volume * 100)}%`, 1500); } // ] key - increase volume if (e.key === ']') { SpaceMusic.setVolume(SpaceMusic.volume + 0.05); SpaceMusicController.showNotification(`🔊 Volume: ${Math.round(SpaceMusic.volume * 100)}%`, 1500); } }); // ═══════════════════════════════════════════════════════════════════════════════════════ // v12.13: MINECRAFT-STYLE BUILDER MODE // Full creative building system with grid-based block placement // Place blocks like LEGO/Minecraft to create structures in the world // Features: Block palette, ghost preview, grid snapping, save/load structures // ═══════════════════════════════════════════════════════════════════════════════════════ const BuilderMode = { // State active: false, selectedBlockIndex: 0, placedBlocks: [], // All placed blocks in current world blockMeshes: [], // Three.js meshes for placed blocks ghostBlock: null, // Preview block mesh ghostBlocks: [], // Multiple ghost blocks for brush sizes gridHelper: null, // Visual grid on ground lastPlacementPos: null, // Prevent rapid duplicate placement // v7.83: Pre-allocated Vector3 objects to avoid GC pressure _tempCenter: null, // Reused for symmetry center calculation _tempSnapped: null, // Reused for snapToGrid result _tempGroundPlane: null, // Reused for ground plane raycast _tempGroundIntersect: null, // Reused for ground intersection _tempNormal: null, // Reused for hit normal _statusBarCache: null, // Cached DOM references for status bar // Grid settings gridSize: 1, // 1 unit per block maxHeight: 100, // Maximum build height (increased) gridLevel: 0, // Current grid Y level // Undo/Redo system undoStack: [], // Stack of actions for undo redoStack: [], // Stack of actions for redo maxUndoSteps: 100, // Maximum undo history // Tool modes currentTool: 'place', // place, fill, replace, select, line, wall brushSize: 1, // 1x1, 2x2, 3x3 symmetryMode: 'none', // none, x, z, xz (mirror modes) continuousPlace: false, // Hold click to place continuously isPlacing: false, // Currently holding mouse for continuous placement // Selection system selectionStart: null, // First corner of selection box selectionEnd: null, // Second corner of selection box selectionBox: null, // Visual selection box mesh clipboard: [], // Copied blocks // Fill tool state fillStart: null, fillEnd: null, // Line/Wall tool lineStart: null, // Favorites favoriteBlocks: [], // Array of favorite block IDs // Category filter currentCategory: 'all', // Current category filter // Block categories blockCategories: { natural: { name: 'Natural', icon: '🌿', blocks: ['stone', 'grass', 'dirt', 'sand', 'gravel', 'clay', 'snow', 'ice', 'moss', 'mud'] }, wood: { name: 'Wood', icon: '🪵', blocks: ['oak_wood', 'birch_wood', 'dark_wood', 'jungle_wood', 'acacia_wood', 'oak_planks', 'birch_planks', 'dark_planks'] }, stone: { name: 'Stone', icon: '🪨', blocks: ['cobblestone', 'stone_brick', 'mossy_stone', 'cracked_stone', 'chiseled_stone', 'slate', 'granite', 'andesite', 'diorite', 'basalt'] }, building: { name: 'Building', icon: '🏗️', blocks: ['brick', 'concrete_white', 'concrete_gray', 'concrete_black', 'terracotta', 'sandstone', 'red_sandstone', 'quartz', 'prismarine'] }, metal: { name: 'Metal', icon: '⚙️', blocks: ['iron', 'gold', 'copper', 'bronze', 'steel', 'titanium', 'chrome', 'rusted_iron'] }, glass: { name: 'Glass', icon: '🔮', blocks: ['glass', 'tinted_glass', 'glass_red', 'glass_blue', 'glass_green', 'glass_yellow', 'glass_purple', 'glass_orange'] }, lights: { name: 'Lights', icon: '💡', blocks: ['glowstone', 'lamp_white', 'lamp_warm', 'lamp_cool', 'neon_pink', 'neon_cyan', 'neon_green', 'neon_yellow', 'neon_red', 'neon_purple'] }, gems: { name: 'Gems', icon: '💎', blocks: ['diamond', 'emerald', 'ruby', 'sapphire', 'amethyst', 'topaz', 'crystal', 'obsidian'] }, special: { name: 'Special', icon: '✨', blocks: ['lava', 'water', 'portal', 'void', 'hologram', 'forcefield', 'energy', 'plasma'] }, alien: { name: 'Alien', icon: '👽', blocks: ['alien', 'biomass', 'xenolith', 'corrupted', 'hivemind', 'spore', 'carapace', 'membrane'] }, tech: { name: 'Tech', icon: '🤖', blocks: ['circuit', 'server', 'display', 'solar', 'battery', 'conduit', 'antenna', 'reactor'] }, colors: { name: 'Colors', icon: '🎨', blocks: ['wool_white', 'wool_red', 'wool_orange', 'wool_yellow', 'wool_green', 'wool_cyan', 'wool_blue', 'wool_purple', 'wool_pink', 'wool_black'] } }, // Expanded block types (60+ blocks with categories) blockTypes: [ // Natural { id: 'stone', name: 'Stone', category: 'natural', color: 0x888888, emissive: 0x000000, roughness: 0.8, metalness: 0.1 }, { id: 'grass', name: 'Grass', category: 'natural', color: 0x33aa33, emissive: 0x000000, roughness: 0.9, metalness: 0.0 }, { id: 'dirt', name: 'Dirt', category: 'natural', color: 0x8B4513, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'sand', name: 'Sand', category: 'natural', color: 0xeeddaa, emissive: 0x000000, roughness: 0.95, metalness: 0.0 }, { id: 'gravel', name: 'Gravel', category: 'natural', color: 0x777777, emissive: 0x000000, roughness: 0.95, metalness: 0.05 }, { id: 'clay', name: 'Clay', category: 'natural', color: 0x9999aa, emissive: 0x000000, roughness: 0.85, metalness: 0.0 }, { id: 'snow', name: 'Snow', category: 'natural', color: 0xffffff, emissive: 0x111111, roughness: 0.7, metalness: 0.0 }, { id: 'ice', name: 'Ice', category: 'natural', color: 0xaaddff, emissive: 0x113344, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.85 }, { id: 'moss', name: 'Moss', category: 'natural', color: 0x446633, emissive: 0x000000, roughness: 0.95, metalness: 0.0 }, { id: 'mud', name: 'Mud', category: 'natural', color: 0x553322, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, // Wood { id: 'oak_wood', name: 'Oak Log', category: 'wood', color: 0x8B5A2B, emissive: 0x000000, roughness: 0.85, metalness: 0.0 }, { id: 'birch_wood', name: 'Birch Log', category: 'wood', color: 0xddccaa, emissive: 0x000000, roughness: 0.85, metalness: 0.0 }, { id: 'dark_wood', name: 'Dark Oak', category: 'wood', color: 0x3d2817, emissive: 0x000000, roughness: 0.85, metalness: 0.0 }, { id: 'jungle_wood', name: 'Jungle', category: 'wood', color: 0x6b4423, emissive: 0x000000, roughness: 0.85, metalness: 0.0 }, { id: 'acacia_wood', name: 'Acacia', category: 'wood', color: 0xa05030, emissive: 0x000000, roughness: 0.85, metalness: 0.0 }, { id: 'oak_planks', name: 'Oak Plank', category: 'wood', color: 0xbc9862, emissive: 0x000000, roughness: 0.8, metalness: 0.0 }, { id: 'birch_planks', name: 'Birch Plank', category: 'wood', color: 0xd5c98c, emissive: 0x000000, roughness: 0.8, metalness: 0.0 }, { id: 'dark_planks', name: 'Dark Plank', category: 'wood', color: 0x4a3728, emissive: 0x000000, roughness: 0.8, metalness: 0.0 }, // Stone { id: 'cobblestone', name: 'Cobble', category: 'stone', color: 0x666666, emissive: 0x000000, roughness: 0.9, metalness: 0.05 }, { id: 'stone_brick', name: 'Stone Brick', category: 'stone', color: 0x7a7a7a, emissive: 0x000000, roughness: 0.75, metalness: 0.05 }, { id: 'mossy_stone', name: 'Mossy Stone', category: 'stone', color: 0x5a6a5a, emissive: 0x000000, roughness: 0.85, metalness: 0.05 }, { id: 'cracked_stone', name: 'Cracked', category: 'stone', color: 0x606060, emissive: 0x000000, roughness: 0.9, metalness: 0.05 }, { id: 'chiseled_stone', name: 'Chiseled', category: 'stone', color: 0x8a8a8a, emissive: 0x000000, roughness: 0.6, metalness: 0.1 }, { id: 'slate', name: 'Slate', category: 'stone', color: 0x4a4a55, emissive: 0x000000, roughness: 0.7, metalness: 0.1 }, { id: 'granite', name: 'Granite', category: 'stone', color: 0x9a7a6a, emissive: 0x000000, roughness: 0.75, metalness: 0.1 }, { id: 'andesite', name: 'Andesite', category: 'stone', color: 0x8a8a8a, emissive: 0x000000, roughness: 0.8, metalness: 0.05 }, { id: 'diorite', name: 'Diorite', category: 'stone', color: 0xbbbbbb, emissive: 0x000000, roughness: 0.75, metalness: 0.05 }, { id: 'basalt', name: 'Basalt', category: 'stone', color: 0x3a3a3a, emissive: 0x000000, roughness: 0.85, metalness: 0.1 }, // Building { id: 'brick', name: 'Brick', category: 'building', color: 0xaa4444, emissive: 0x000000, roughness: 0.75, metalness: 0.1 }, { id: 'concrete_white', name: 'White Block', category: 'building', color: 0xeeeeee, emissive: 0x000000, roughness: 0.9, metalness: 0.0 }, { id: 'concrete_gray', name: 'Gray Block', category: 'building', color: 0x888888, emissive: 0x000000, roughness: 0.9, metalness: 0.0 }, { id: 'concrete_black', name: 'Black Block', category: 'building', color: 0x222222, emissive: 0x000000, roughness: 0.9, metalness: 0.0 }, { id: 'terracotta', name: 'Terracotta', category: 'building', color: 0xc67a53, emissive: 0x000000, roughness: 0.85, metalness: 0.0 }, { id: 'sandstone', name: 'Sandstone', category: 'building', color: 0xdcc9a0, emissive: 0x000000, roughness: 0.8, metalness: 0.0 }, { id: 'red_sandstone', name: 'Red Sand', category: 'building', color: 0xc47a4a, emissive: 0x000000, roughness: 0.8, metalness: 0.0 }, { id: 'quartz', name: 'Quartz', category: 'building', color: 0xf5f0e8, emissive: 0x111111, roughness: 0.3, metalness: 0.2 }, { id: 'prismarine', name: 'Prismarine', category: 'building', color: 0x5aa090, emissive: 0x113322, roughness: 0.6, metalness: 0.2 }, // Metal { id: 'iron', name: 'Iron', category: 'metal', color: 0xcccccc, emissive: 0x000000, roughness: 0.4, metalness: 0.85 }, { id: 'gold', name: 'Gold', category: 'metal', color: 0xffd700, emissive: 0x332200, roughness: 0.2, metalness: 0.9 }, { id: 'copper', name: 'Copper', category: 'metal', color: 0xb87333, emissive: 0x110500, roughness: 0.35, metalness: 0.85 }, { id: 'bronze', name: 'Bronze', category: 'metal', color: 0xcd7f32, emissive: 0x110800, roughness: 0.4, metalness: 0.8 }, { id: 'steel', name: 'Steel', category: 'metal', color: 0x8a9ea8, emissive: 0x000000, roughness: 0.3, metalness: 0.9 }, { id: 'titanium', name: 'Titanium', category: 'metal', color: 0xbec2cb, emissive: 0x050508, roughness: 0.25, metalness: 0.95 }, { id: 'chrome', name: 'Chrome', category: 'metal', color: 0xe8e8e8, emissive: 0x111111, roughness: 0.1, metalness: 1.0 }, { id: 'rusted_iron', name: 'Rust', category: 'metal', color: 0x8b4513, emissive: 0x000000, roughness: 0.9, metalness: 0.5 }, // Glass { id: 'glass', name: 'Glass', category: 'glass', color: 0xffffff, emissive: 0x000000, roughness: 0.0, metalness: 0.1, transparent: true, opacity: 0.3 }, { id: 'tinted_glass', name: 'Tinted', category: 'glass', color: 0x333333, emissive: 0x000000, roughness: 0.0, metalness: 0.1, transparent: true, opacity: 0.5 }, { id: 'glass_red', name: 'Red Glass', category: 'glass', color: 0xff3333, emissive: 0x220000, roughness: 0.0, metalness: 0.1, transparent: true, opacity: 0.5 }, { id: 'glass_blue', name: 'Blue Glass', category: 'glass', color: 0x3333ff, emissive: 0x000022, roughness: 0.0, metalness: 0.1, transparent: true, opacity: 0.5 }, { id: 'glass_green', name: 'Green Glass', category: 'glass', color: 0x33ff33, emissive: 0x002200, roughness: 0.0, metalness: 0.1, transparent: true, opacity: 0.5 }, { id: 'glass_yellow', name: 'Yellow Glass', category: 'glass', color: 0xffff33, emissive: 0x222200, roughness: 0.0, metalness: 0.1, transparent: true, opacity: 0.5 }, { id: 'glass_purple', name: 'Purple Glass', category: 'glass', color: 0xaa33ff, emissive: 0x110022, roughness: 0.0, metalness: 0.1, transparent: true, opacity: 0.5 }, { id: 'glass_orange', name: 'Orange Glass', category: 'glass', color: 0xff8833, emissive: 0x221100, roughness: 0.0, metalness: 0.1, transparent: true, opacity: 0.5 }, // Lights { id: 'glowstone', name: 'Glow', category: 'lights', color: 0xffdd88, emissive: 0xffaa44, roughness: 0.6, metalness: 0.0, emissiveIntensity: 0.8 }, { id: 'lamp_white', name: 'White Lamp', category: 'lights', color: 0xffffff, emissive: 0xffffff, roughness: 0.5, metalness: 0.0, emissiveIntensity: 1.0 }, { id: 'lamp_warm', name: 'Warm Lamp', category: 'lights', color: 0xffddaa, emissive: 0xffaa66, roughness: 0.5, metalness: 0.0, emissiveIntensity: 1.0 }, { id: 'lamp_cool', name: 'Cool Lamp', category: 'lights', color: 0xaaddff, emissive: 0x66aaff, roughness: 0.5, metalness: 0.0, emissiveIntensity: 1.0 }, { id: 'neon_pink', name: 'Neon Pink', category: 'lights', color: 0xff00ff, emissive: 0xff00ff, roughness: 0.3, metalness: 0.2, emissiveIntensity: 0.9 }, { id: 'neon_cyan', name: 'Neon Cyan', category: 'lights', color: 0x00ffff, emissive: 0x00ffff, roughness: 0.3, metalness: 0.2, emissiveIntensity: 0.9 }, { id: 'neon_green', name: 'Neon Green', category: 'lights', color: 0x00ff00, emissive: 0x00ff00, roughness: 0.3, metalness: 0.2, emissiveIntensity: 0.9 }, { id: 'neon_yellow', name: 'Neon Yellow', category: 'lights', color: 0xffff00, emissive: 0xffff00, roughness: 0.3, metalness: 0.2, emissiveIntensity: 0.9 }, { id: 'neon_red', name: 'Neon Red', category: 'lights', color: 0xff0000, emissive: 0xff0000, roughness: 0.3, metalness: 0.2, emissiveIntensity: 0.9 }, { id: 'neon_purple', name: 'Neon Purple', category: 'lights', color: 0x8800ff, emissive: 0x8800ff, roughness: 0.3, metalness: 0.2, emissiveIntensity: 0.9 }, // Gems { id: 'diamond', name: 'Diamond', category: 'gems', color: 0x88ffff, emissive: 0x44aaaa, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.9, emissiveIntensity: 0.4 }, { id: 'emerald', name: 'Emerald', category: 'gems', color: 0x00ff55, emissive: 0x00aa33, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.9, emissiveIntensity: 0.4 }, { id: 'ruby', name: 'Ruby', category: 'gems', color: 0xff2244, emissive: 0xaa1133, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.9, emissiveIntensity: 0.4 }, { id: 'sapphire', name: 'Sapphire', category: 'gems', color: 0x2244ff, emissive: 0x1133aa, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.9, emissiveIntensity: 0.4 }, { id: 'amethyst', name: 'Amethyst', category: 'gems', color: 0xaa44ff, emissive: 0x6622aa, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.9, emissiveIntensity: 0.4 }, { id: 'topaz', name: 'Topaz', category: 'gems', color: 0xffaa22, emissive: 0xaa6611, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.9, emissiveIntensity: 0.4 }, { id: 'crystal', name: 'Crystal', category: 'gems', color: 0x00ffcc, emissive: 0x00aa88, roughness: 0.1, metalness: 0.4, transparent: true, opacity: 0.8, emissiveIntensity: 0.6 }, { id: 'obsidian', name: 'Obsidian', category: 'gems', color: 0x1a0a2e, emissive: 0x220044, roughness: 0.3, metalness: 0.5 }, // Special { id: 'lava', name: 'Lava', category: 'special', color: 0xff4400, emissive: 0xff2200, roughness: 0.8, metalness: 0.0, emissiveIntensity: 1.0 }, { id: 'water', name: 'Water', category: 'special', color: 0x2266aa, emissive: 0x001133, roughness: 0.1, metalness: 0.2, transparent: true, opacity: 0.7 }, { id: 'portal', name: 'Portal', category: 'special', color: 0x8800ff, emissive: 0x8800ff, roughness: 0.0, metalness: 0.1, transparent: true, opacity: 0.8, emissiveIntensity: 1.2 }, { id: 'void', name: 'Void', category: 'special', color: 0x000011, emissive: 0x110022, roughness: 0.0, metalness: 0.0, emissiveIntensity: 0.2 }, { id: 'hologram', name: 'Hologram', category: 'special', color: 0x00ffff, emissive: 0x00ffff, roughness: 0.0, metalness: 0.0, transparent: true, opacity: 0.4, emissiveIntensity: 0.8 }, { id: 'forcefield', name: 'Forcefield', category: 'special', color: 0x00aaff, emissive: 0x0066ff, roughness: 0.0, metalness: 0.0, transparent: true, opacity: 0.3, emissiveIntensity: 0.6 }, { id: 'energy', name: 'Energy', category: 'special', color: 0xffff00, emissive: 0xffaa00, roughness: 0.0, metalness: 0.0, transparent: true, opacity: 0.6, emissiveIntensity: 1.0 }, { id: 'plasma', name: 'Plasma', category: 'special', color: 0xff00aa, emissive: 0xff0088, roughness: 0.0, metalness: 0.0, transparent: true, opacity: 0.5, emissiveIntensity: 1.0 }, // Alien { id: 'alien', name: 'Alien', category: 'alien', color: 0x8800ff, emissive: 0x440088, roughness: 0.5, metalness: 0.3, emissiveIntensity: 0.5 }, { id: 'biomass', name: 'Biomass', category: 'alien', color: 0x44aa44, emissive: 0x226622, roughness: 0.8, metalness: 0.0, emissiveIntensity: 0.3 }, { id: 'xenolith', name: 'Xenolith', category: 'alien', color: 0x334455, emissive: 0x112233, roughness: 0.6, metalness: 0.4, emissiveIntensity: 0.2 }, { id: 'corrupted', name: 'Corrupted', category: 'alien', color: 0x440044, emissive: 0x220022, roughness: 0.7, metalness: 0.2, emissiveIntensity: 0.4 }, { id: 'hivemind', name: 'Hivemind', category: 'alien', color: 0x884400, emissive: 0x442200, roughness: 0.9, metalness: 0.1, emissiveIntensity: 0.3 }, { id: 'spore', name: 'Spore', category: 'alien', color: 0x88ff88, emissive: 0x44aa44, roughness: 0.7, metalness: 0.0, emissiveIntensity: 0.5 }, { id: 'carapace', name: 'Carapace', category: 'alien', color: 0x223344, emissive: 0x111122, roughness: 0.3, metalness: 0.6, emissiveIntensity: 0.2 }, { id: 'membrane', name: 'Membrane', category: 'alien', color: 0xaaff88, emissive: 0x55aa44, roughness: 0.2, metalness: 0.0, transparent: true, opacity: 0.6, emissiveIntensity: 0.4 }, // Tech { id: 'circuit', name: 'Circuit', category: 'tech', color: 0x115511, emissive: 0x00ff00, roughness: 0.5, metalness: 0.4, emissiveIntensity: 0.3 }, { id: 'server', name: 'Server', category: 'tech', color: 0x333344, emissive: 0x0044ff, roughness: 0.4, metalness: 0.7, emissiveIntensity: 0.2 }, { id: 'display', name: 'Display', category: 'tech', color: 0x111122, emissive: 0x0088ff, roughness: 0.1, metalness: 0.2, emissiveIntensity: 0.6 }, { id: 'solar', name: 'Solar', category: 'tech', color: 0x112244, emissive: 0x0044aa, roughness: 0.3, metalness: 0.5, emissiveIntensity: 0.3 }, { id: 'battery', name: 'Battery', category: 'tech', color: 0x444444, emissive: 0x00ff44, roughness: 0.5, metalness: 0.6, emissiveIntensity: 0.4 }, { id: 'conduit', name: 'Conduit', category: 'tech', color: 0x555566, emissive: 0x00aaff, roughness: 0.3, metalness: 0.7, emissiveIntensity: 0.3 }, { id: 'antenna', name: 'Antenna', category: 'tech', color: 0x888899, emissive: 0xff0044, roughness: 0.2, metalness: 0.8, emissiveIntensity: 0.4 }, { id: 'reactor', name: 'Reactor', category: 'tech', color: 0x333333, emissive: 0x00ffff, roughness: 0.4, metalness: 0.6, emissiveIntensity: 0.8 }, // Colors (Wool-like) { id: 'wool_white', name: 'White', category: 'colors', color: 0xffffff, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'wool_red', name: 'Red', category: 'colors', color: 0xcc3333, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'wool_orange', name: 'Orange', category: 'colors', color: 0xff8833, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'wool_yellow', name: 'Yellow', category: 'colors', color: 0xffdd33, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'wool_green', name: 'Green', category: 'colors', color: 0x33cc33, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'wool_cyan', name: 'Cyan', category: 'colors', color: 0x33cccc, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'wool_blue', name: 'Blue', category: 'colors', color: 0x3333cc, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'wool_purple', name: 'Purple', category: 'colors', color: 0x8833cc, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'wool_pink', name: 'Pink', category: 'colors', color: 0xff88cc, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, { id: 'wool_black', name: 'Black', category: 'colors', color: 0x222222, emissive: 0x000000, roughness: 1.0, metalness: 0.0 }, // Marble and decorative { id: 'marble', name: 'Marble', category: 'building', color: 0xf5f5f5, emissive: 0x000000, roughness: 0.2, metalness: 0.1 } ], // Prefab templates for quick structure placement prefabs: { house: { name: 'Small House', icon: '🏠', blocks: [ // Floor {x:0,y:0,z:0,t:'oak_planks'},{x:1,y:0,z:0,t:'oak_planks'},{x:2,y:0,z:0,t:'oak_planks'},{x:3,y:0,z:0,t:'oak_planks'},{x:4,y:0,z:0,t:'oak_planks'}, {x:0,y:0,z:1,t:'oak_planks'},{x:1,y:0,z:1,t:'oak_planks'},{x:2,y:0,z:1,t:'oak_planks'},{x:3,y:0,z:1,t:'oak_planks'},{x:4,y:0,z:1,t:'oak_planks'}, {x:0,y:0,z:2,t:'oak_planks'},{x:1,y:0,z:2,t:'oak_planks'},{x:2,y:0,z:2,t:'oak_planks'},{x:3,y:0,z:2,t:'oak_planks'},{x:4,y:0,z:2,t:'oak_planks'}, {x:0,y:0,z:3,t:'oak_planks'},{x:1,y:0,z:3,t:'oak_planks'},{x:2,y:0,z:3,t:'oak_planks'},{x:3,y:0,z:3,t:'oak_planks'},{x:4,y:0,z:3,t:'oak_planks'}, {x:0,y:0,z:4,t:'oak_planks'},{x:1,y:0,z:4,t:'oak_planks'},{x:2,y:0,z:4,t:'oak_planks'},{x:3,y:0,z:4,t:'oak_planks'},{x:4,y:0,z:4,t:'oak_planks'}, // Walls {x:0,y:1,z:0,t:'oak_wood'},{x:0,y:2,z:0,t:'oak_wood'},{x:0,y:3,z:0,t:'oak_wood'}, {x:4,y:1,z:0,t:'oak_wood'},{x:4,y:2,z:0,t:'oak_wood'},{x:4,y:3,z:0,t:'oak_wood'}, {x:0,y:1,z:4,t:'oak_wood'},{x:0,y:2,z:4,t:'oak_wood'},{x:0,y:3,z:4,t:'oak_wood'}, {x:4,y:1,z:4,t:'oak_wood'},{x:4,y:2,z:4,t:'oak_wood'},{x:4,y:3,z:4,t:'oak_wood'}, // Front wall with door space {x:1,y:1,z:0,t:'oak_planks'},{x:1,y:2,z:0,t:'oak_planks'},{x:1,y:3,z:0,t:'oak_planks'}, {x:3,y:1,z:0,t:'oak_planks'},{x:3,y:2,z:0,t:'oak_planks'},{x:3,y:3,z:0,t:'oak_planks'}, // Back wall {x:1,y:1,z:4,t:'oak_planks'},{x:1,y:2,z:4,t:'oak_planks'},{x:1,y:3,z:4,t:'oak_planks'}, {x:2,y:1,z:4,t:'oak_planks'},{x:2,y:2,z:4,t:'glass'},{x:2,y:3,z:4,t:'oak_planks'}, {x:3,y:1,z:4,t:'oak_planks'},{x:3,y:2,z:4,t:'oak_planks'},{x:3,y:3,z:4,t:'oak_planks'}, // Side walls {x:0,y:1,z:1,t:'oak_planks'},{x:0,y:2,z:1,t:'glass'},{x:0,y:3,z:1,t:'oak_planks'}, {x:0,y:1,z:2,t:'oak_planks'},{x:0,y:2,z:2,t:'oak_planks'},{x:0,y:3,z:2,t:'oak_planks'}, {x:0,y:1,z:3,t:'oak_planks'},{x:0,y:2,z:3,t:'glass'},{x:0,y:3,z:3,t:'oak_planks'}, {x:4,y:1,z:1,t:'oak_planks'},{x:4,y:2,z:1,t:'glass'},{x:4,y:3,z:1,t:'oak_planks'}, {x:4,y:1,z:2,t:'oak_planks'},{x:4,y:2,z:2,t:'oak_planks'},{x:4,y:3,z:2,t:'oak_planks'}, {x:4,y:1,z:3,t:'oak_planks'},{x:4,y:2,z:3,t:'glass'},{x:4,y:3,z:3,t:'oak_planks'}, // Roof {x:0,y:4,z:0,t:'brick'},{x:1,y:4,z:0,t:'brick'},{x:2,y:4,z:0,t:'brick'},{x:3,y:4,z:0,t:'brick'},{x:4,y:4,z:0,t:'brick'}, {x:0,y:4,z:1,t:'brick'},{x:1,y:4,z:1,t:'brick'},{x:2,y:4,z:1,t:'brick'},{x:3,y:4,z:1,t:'brick'},{x:4,y:4,z:1,t:'brick'}, {x:0,y:4,z:2,t:'brick'},{x:1,y:4,z:2,t:'brick'},{x:2,y:4,z:2,t:'brick'},{x:3,y:4,z:2,t:'brick'},{x:4,y:4,z:2,t:'brick'}, {x:0,y:4,z:3,t:'brick'},{x:1,y:4,z:3,t:'brick'},{x:2,y:4,z:3,t:'brick'},{x:3,y:4,z:3,t:'brick'},{x:4,y:4,z:3,t:'brick'}, {x:0,y:4,z:4,t:'brick'},{x:1,y:4,z:4,t:'brick'},{x:2,y:4,z:4,t:'brick'},{x:3,y:4,z:4,t:'brick'},{x:4,y:4,z:4,t:'brick'}, // Light inside {x:2,y:3,z:2,t:'glowstone'} ] }, tower: { name: 'Watch Tower', icon: '🗼', blocks: [ // Base {x:0,y:0,z:0,t:'stone_brick'},{x:1,y:0,z:0,t:'stone_brick'},{x:2,y:0,z:0,t:'stone_brick'}, {x:0,y:0,z:1,t:'stone_brick'},{x:1,y:0,z:1,t:'stone_brick'},{x:2,y:0,z:1,t:'stone_brick'}, {x:0,y:0,z:2,t:'stone_brick'},{x:1,y:0,z:2,t:'stone_brick'},{x:2,y:0,z:2,t:'stone_brick'}, // Walls level 1-6 ...Array.from({length:6}, (_,y) => [ {x:0,y:y+1,z:0,t:'stone_brick'},{x:2,y:y+1,z:0,t:'stone_brick'}, {x:0,y:y+1,z:2,t:'stone_brick'},{x:2,y:y+1,z:2,t:'stone_brick'} ]).flat(), // Battlements {x:0,y:7,z:0,t:'stone_brick'},{x:2,y:7,z:0,t:'stone_brick'}, {x:0,y:7,z:2,t:'stone_brick'},{x:2,y:7,z:2,t:'stone_brick'}, {x:1,y:7,z:0,t:'stone_brick'},{x:1,y:7,z:2,t:'stone_brick'}, {x:0,y:7,z:1,t:'stone_brick'},{x:2,y:7,z:1,t:'stone_brick'}, // Light on top {x:1,y:6,z:1,t:'lamp_warm'} ] }, tree: { name: 'Oak Tree', icon: '🌳', blocks: [ // Trunk {x:0,y:0,z:0,t:'oak_wood'},{x:0,y:1,z:0,t:'oak_wood'},{x:0,y:2,z:0,t:'oak_wood'},{x:0,y:3,z:0,t:'oak_wood'}, // Leaves {x:-1,y:3,z:-1,t:'grass'},{x:0,y:3,z:-1,t:'grass'},{x:1,y:3,z:-1,t:'grass'}, {x:-1,y:3,z:0,t:'grass'},{x:1,y:3,z:0,t:'grass'}, {x:-1,y:3,z:1,t:'grass'},{x:0,y:3,z:1,t:'grass'},{x:1,y:3,z:1,t:'grass'}, {x:-1,y:4,z:-1,t:'grass'},{x:0,y:4,z:-1,t:'grass'},{x:1,y:4,z:-1,t:'grass'}, {x:-1,y:4,z:0,t:'grass'},{x:0,y:4,z:0,t:'grass'},{x:1,y:4,z:0,t:'grass'}, {x:-1,y:4,z:1,t:'grass'},{x:0,y:4,z:1,t:'grass'},{x:1,y:4,z:1,t:'grass'}, {x:0,y:5,z:0,t:'grass'},{x:0,y:5,z:-1,t:'grass'},{x:-1,y:5,z:0,t:'grass'},{x:1,y:5,z:0,t:'grass'},{x:0,y:5,z:1,t:'grass'} ] }, bridge: { name: 'Bridge', icon: '🌉', blocks: [ // Span ...Array.from({length:8}, (_,x) => ({x,y:0,z:0,t:'oak_planks'})), ...Array.from({length:8}, (_,x) => ({x,y:0,z:1,t:'oak_planks'})), // Rails ...Array.from({length:8}, (_,x) => ({x,y:1,z:0,t:'oak_wood'})), ...Array.from({length:8}, (_,x) => ({x,y:1,z:1,t:'oak_wood'})) ] }, fountain: { name: 'Fountain', icon: '⛲', blocks: [ // Base ring {x:-1,y:0,z:-1,t:'stone_brick'},{x:0,y:0,z:-1,t:'stone_brick'},{x:1,y:0,z:-1,t:'stone_brick'}, {x:-1,y:0,z:0,t:'stone_brick'},{x:0,y:0,z:0,t:'water'},{x:1,y:0,z:0,t:'stone_brick'}, {x:-1,y:0,z:1,t:'stone_brick'},{x:0,y:0,z:1,t:'stone_brick'},{x:1,y:0,z:1,t:'stone_brick'}, // Walls {x:-1,y:1,z:-1,t:'stone_brick'},{x:1,y:1,z:-1,t:'stone_brick'}, {x:-1,y:1,z:1,t:'stone_brick'},{x:1,y:1,z:1,t:'stone_brick'}, // Center spout {x:0,y:1,z:0,t:'quartz'},{x:0,y:2,z:0,t:'water'} ] }, lamp_post: { name: 'Lamp Post', icon: '🏮', blocks: [ {x:0,y:0,z:0,t:'iron'},{x:0,y:1,z:0,t:'iron'},{x:0,y:2,z:0,t:'iron'}, {x:0,y:3,z:0,t:'lamp_warm'} ] }, portal_frame: { name: 'Portal Frame', icon: '🌀', blocks: [ // Frame {x:0,y:0,z:0,t:'obsidian'},{x:1,y:0,z:0,t:'obsidian'},{x:2,y:0,z:0,t:'obsidian'},{x:3,y:0,z:0,t:'obsidian'}, {x:0,y:1,z:0,t:'obsidian'},{x:3,y:1,z:0,t:'obsidian'}, {x:0,y:2,z:0,t:'obsidian'},{x:3,y:2,z:0,t:'obsidian'}, {x:0,y:3,z:0,t:'obsidian'},{x:3,y:3,z:0,t:'obsidian'}, {x:0,y:4,z:0,t:'obsidian'},{x:1,y:4,z:0,t:'obsidian'},{x:2,y:4,z:0,t:'obsidian'},{x:3,y:4,z:0,t:'obsidian'}, // Portal fill {x:1,y:1,z:0,t:'portal'},{x:2,y:1,z:0,t:'portal'}, {x:1,y:2,z:0,t:'portal'},{x:2,y:2,z:0,t:'portal'}, {x:1,y:3,z:0,t:'portal'},{x:2,y:3,z:0,t:'portal'} ] }, pyramid: { name: 'Pyramid', icon: '🔺', blocks: [ // Layer 0 (5x5) ...Array.from({length:5}, (_,x) => Array.from({length:5}, (_,z) => ({x:x-2,y:0,z:z-2,t:'sandstone'}))).flat(), // Layer 1 (3x3) ...Array.from({length:3}, (_,x) => Array.from({length:3}, (_,z) => ({x:x-1,y:1,z:z-1,t:'sandstone'}))).flat(), // Layer 2 (1x1 - top) {x:0,y:2,z:0,t:'gold'} ] } }, // Cached geometries and materials for performance blockGeometry: null, materialCache: {}, // Initialize builder mode resources init() { // v7.83: Pre-allocate Vector3 objects for symmetry/snap calculations this._tempCenter = new THREE.Vector3(); this._tempSnapped = new THREE.Vector3(); this._tempGroundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); this._tempGroundIntersect = new THREE.Vector3(); this._tempNormal = new THREE.Vector3(0, 1, 0); // Create shared block geometry this.blockGeometry = new THREE.BoxGeometry(this.gridSize * 0.98, this.gridSize * 0.98, this.gridSize * 0.98); // Pre-create materials for all block types this.blockTypes.forEach(block => { const matOptions = { color: block.color, roughness: block.roughness, metalness: block.metalness }; if (block.emissive) { matOptions.emissive = block.emissive; matOptions.emissiveIntensity = block.emissiveIntensity || 0.3; } if (block.transparent) { matOptions.transparent = true; matOptions.opacity = block.opacity; } this.materialCache[block.id] = new THREE.MeshStandardMaterial(matOptions); }); // Create ghost block for preview this.createGhostBlock(); // Create UI this.createUI(); // Load favorites from localStorage this.loadFavorites(); Logger.info('BuilderMode', `Initialized with ${this.blockTypes.length} block types and ${Object.keys(this.prefabs).length} prefabs`); }, // Create ghost preview block createGhostBlock() { const ghostMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, transparent: true, opacity: 0.4, emissive: 0x00ffff, emissiveIntensity: 0.3, wireframe: false }); this.ghostBlock = new THREE.Mesh(this.blockGeometry, ghostMaterial); this.ghostBlock.visible = false; this.ghostBlock.renderOrder = 999; // Add wireframe overlay for clarity const wireGeo = new THREE.EdgesGeometry(this.blockGeometry); const wireMat = new THREE.LineBasicMaterial({ color: 0x00ffff, linewidth: 2 }); const wireframe = new THREE.LineSegments(wireGeo, wireMat); this.ghostBlock.add(wireframe); }, // Create builder UI (hotbar + controls) createUI() { // Main builder panel const panel = document.createElement('div'); panel.id = 'builder-panel'; panel.innerHTML = `
Tool: Place Brush: 1x1 Symmetry: Off Grid Y: 0 Blocks: 0
Page 1/3
`; document.body.appendChild(panel); // Prefab menu const prefabMenu = document.createElement('div'); prefabMenu.id = 'builder-prefab-menu'; prefabMenu.innerHTML = `
📦 Prefab Structures
`; document.body.appendChild(prefabMenu); // Settings menu const settingsMenu = document.createElement('div'); settingsMenu.id = 'builder-settings-menu'; settingsMenu.innerHTML = `
⚙️ Builder Settings
Brush Size
Symmetry Mode
Grid Level (Y)
0
Quick Actions
`; document.body.appendChild(settingsMenu); // Populate category buttons this.populateCategoryButtons(); // Populate prefab grid this.populatePrefabGrid(); // Toggle button const toggleBtn = document.createElement('button'); toggleBtn.id = 'builder-toggle-btn'; toggleBtn.innerHTML = '🧱 Builder'; toggleBtn.onclick = () => this.toggle(); document.body.appendChild(toggleBtn); // Block count display const countDisplay = document.createElement('div'); countDisplay.id = 'builder-block-count'; countDisplay.innerHTML = 'Blocks: 0'; document.body.appendChild(countDisplay); // Populate hotbar this.currentPage = 0; this.blocksPerPage = 10; this.updateHotbar(); }, // Get filtered blocks based on current category and search getFilteredBlocks() { let blocks = this.blockTypes; // Filter by category if (this.currentCategory !== 'all') { blocks = blocks.filter(b => b.category === this.currentCategory); } // Filter by search term if (this.searchTerm && this.searchTerm.length > 0) { const term = this.searchTerm.toLowerCase(); blocks = blocks.filter(b => b.name.toLowerCase().includes(term) || b.id.toLowerCase().includes(term) || (b.category && b.category.toLowerCase().includes(term)) ); } return blocks; }, // Update hotbar display updateHotbar() { const hotbar = document.getElementById('builder-hotbar'); if (!hotbar) return; hotbar.innerHTML = ''; const filteredBlocks = this.getFilteredBlocks(); const startIdx = this.currentPage * this.blocksPerPage; const endIdx = Math.min(startIdx + this.blocksPerPage, filteredBlocks.length); for (let i = startIdx; i < endIdx; i++) { const block = filteredBlocks[i]; const globalIndex = this.blockTypes.indexOf(block); const slotNum = i - startIdx + 1; const slot = document.createElement('div'); const isFavorite = this.favoriteBlocks.includes(block.id); slot.className = 'builder-slot' + (this.selectedBlockIndex === globalIndex ? ' selected' : '') + (isFavorite ? ' favorite' : ''); slot.onclick = () => this.selectBlock(globalIndex); slot.oncontextmenu = (e) => { e.preventDefault(); this.toggleFavorite(block.id); }; slot.title = `${block.name} (${block.category || 'misc'})\nRight-click to favorite`; // Color preview with 3D-ish effect const colorHex = '#' + block.color.toString(16).padStart(6, '0'); const darkerHex = '#' + Math.max(0, block.color - 0x222222).toString(16).padStart(6, '0'); // Add glow effect for emissive blocks const glowStyle = block.emissiveIntensity > 0.5 ? `box-shadow: 0 0 8px ${colorHex};` : ''; slot.innerHTML = ` ${slotNum <= 9 ? slotNum : ''}
${block.name} `; hotbar.appendChild(slot); } // Update page indicator const pageIndicator = document.getElementById('builder-page-indicator'); if (pageIndicator) { const totalPages = Math.ceil(filteredBlocks.length / this.blocksPerPage) || 1; pageIndicator.textContent = `Page ${this.currentPage + 1}/${totalPages}`; } // Update status bar this.updateStatusBar(); }, nextPage() { const filteredBlocks = this.getFilteredBlocks(); const totalPages = Math.ceil(filteredBlocks.length / this.blocksPerPage) || 1; this.currentPage = (this.currentPage + 1) % totalPages; this.updateHotbar(); }, prevPage() { const filteredBlocks = this.getFilteredBlocks(); const totalPages = Math.ceil(filteredBlocks.length / this.blocksPerPage) || 1; this.currentPage = (this.currentPage - 1 + totalPages) % totalPages; this.updateHotbar(); }, // Select a block type selectBlock(index) { this.selectedBlockIndex = index; this.updateHotbar(); this.updateGhostMaterial(); // Play select sound if (typeof AudioSystem !== 'undefined') { AudioSystem.click(); } }, // ═══════════════════════════════════════════════════════════════════════════ // NEW BUILDER MODE FEATURES // ═══════════════════════════════════════════════════════════════════════════ // Search filter searchTerm: '', searchBlocks(term) { this.searchTerm = term; this.currentPage = 0; this.updateHotbar(); }, // Category system populateCategoryButtons() { const container = document.getElementById('builder-categories'); if (!container) return; // Clear and rebuild container.innerHTML = ''; Object.entries(this.blockCategories).forEach(([key, cat]) => { const btn = document.createElement('button'); btn.className = 'builder-cat-btn'; btn.dataset.cat = key; btn.onclick = () => this.setCategory(key); btn.innerHTML = `${cat.icon} ${cat.name}`; container.appendChild(btn); }); }, setCategory(category) { this.currentCategory = category; this.currentPage = 0; // Update button states document.querySelectorAll('.builder-cat-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.cat === category); }); this.updateHotbar(); if (typeof AudioSystem !== 'undefined') { AudioSystem.click(); } }, // Favorites system toggleFavorite(blockId) { const idx = this.favoriteBlocks.indexOf(blockId); if (idx >= 0) { this.favoriteBlocks.splice(idx, 1); showNotification(`Removed ${blockId} from favorites`, 'info'); } else { this.favoriteBlocks.push(blockId); showNotification(`Added ${blockId} to favorites`, 'info'); } // Save to localStorage localStorage.setItem('leviathan_builder_favorites', JSON.stringify(this.favoriteBlocks)); this.updateHotbar(); }, // v8.0: Now using SafeJSON utility (8-Strategy Consensus Cycle 3) loadFavorites() { this.favoriteBlocks = SafeJSON.fromLocalStorage('leviathan_builder_favorites', []); }, // Tool system setTool(tool) { this.currentTool = tool; // Reset tool-specific state this.selectionStart = null; this.selectionEnd = null; this.fillStart = null; this.fillEnd = null; this.lineStart = null; // Update button states document.querySelectorAll('.builder-tool-btn[data-tool]').forEach(btn => { btn.classList.toggle('active', btn.dataset.tool === tool); }); this.updateStatusBar(); if (typeof AudioSystem !== 'undefined') { AudioSystem.click(); } const toolNames = { place: 'Place', fill: 'Fill', line: 'Line', select: 'Select' }; showNotification(`Tool: ${toolNames[tool] || tool}`, 'info'); }, // Brush size setBrushSize(size) { this.brushSize = size; // Update button states document.querySelectorAll('.settings-btn[data-brush]').forEach(btn => { btn.classList.toggle('active', parseInt(btn.dataset.brush) === size); }); this.updateStatusBar(); this.updateGhostBlocks(); if (typeof AudioSystem !== 'undefined') { AudioSystem.click(); } }, // Update ghost blocks for brush size updateGhostBlocks() { // Remove old ghost blocks this.ghostBlocks.forEach(g => { if (g.parent) g.parent.remove(g); }); this.ghostBlocks = []; // Create new ghost blocks for brush size > 1 if (this.brushSize > 1 && this.active) { const ghostMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, transparent: true, opacity: 0.25, emissive: 0x00ffff, emissiveIntensity: 0.2 }); for (let dx = 0; dx < this.brushSize; dx++) { for (let dz = 0; dz < this.brushSize; dz++) { if (dx === 0 && dz === 0) continue; // Skip center (main ghost) const ghost = new THREE.Mesh(this.blockGeometry, ghostMaterial); ghost.visible = false; this.ghostBlocks.push(ghost); if (typeof scene !== 'undefined') { scene.add(ghost); } } } } }, // Symmetry mode setSymmetry(mode) { this.symmetryMode = mode; // Update button states document.querySelectorAll('.settings-btn[data-sym]').forEach(btn => { btn.classList.toggle('active', btn.dataset.sym === mode); }); this.updateStatusBar(); if (typeof AudioSystem !== 'undefined') { AudioSystem.click(); } const modeNames = { none: 'Off', x: 'X-Axis', z: 'Z-Axis', xz: 'Both Axes' }; showNotification(`Symmetry: ${modeNames[mode]}`, 'info'); }, // Get symmetry positions for a given position // v7.83: Uses pre-allocated _tempCenter vector to reduce GC pressure // v8.24: Uses pooled mirror vectors to reduce clone() allocations getSymmetryPositions(pos) { // v8.24: Lazy-init pooled vectors for symmetry calculations if (!this._mirrorVecs) { this._mirrorVecs = [ new THREE.Vector3(), // original position copy new THREE.Vector3(), // x-mirror new THREE.Vector3(), // z-mirror new THREE.Vector3() // xz-mirror ]; } // v8.24: Copy original position to pooled vector instead of clone() this._mirrorVecs[0].copy(pos); const positions = [this._mirrorVecs[0]]; // v7.83: Reuse pre-allocated center vector if (!this._tempCenter) this._tempCenter = new THREE.Vector3(); const center = this._tempCenter; if (typeof worldState !== 'undefined' && worldState.player) { center.set(worldState.player.position.x, 0, worldState.player.position.z); } else { center.set(0, 0, 0); } if (this.symmetryMode === 'x' || this.symmetryMode === 'xz') { // v8.24: Use pooled vector instead of clone() const mirrored = this._mirrorVecs[1].copy(pos); mirrored.x = center.x - (pos.x - center.x); mirrored.x = Math.round(mirrored.x / this.gridSize) * this.gridSize; positions.push(mirrored); } if (this.symmetryMode === 'z' || this.symmetryMode === 'xz') { // v8.24: Use pooled vector instead of clone() const mirrored = this._mirrorVecs[2].copy(pos); mirrored.z = center.z - (pos.z - center.z); mirrored.z = Math.round(mirrored.z / this.gridSize) * this.gridSize; positions.push(mirrored); } if (this.symmetryMode === 'xz') { // v8.24: Use pooled vector instead of clone() const mirrored = this._mirrorVecs[3].copy(pos); mirrored.x = center.x - (pos.x - center.x); mirrored.z = center.z - (pos.z - center.z); mirrored.x = Math.round(mirrored.x / this.gridSize) * this.gridSize; mirrored.z = Math.round(mirrored.z / this.gridSize) * this.gridSize; positions.push(mirrored); } return positions; }, // Grid level adjustment adjustGridLevel(delta) { this.gridLevel = Math.max(0, Math.min(this.maxHeight, this.gridLevel + delta)); // Update grid position if (this.gridHelper) { this.gridHelper.position.y = this.gridLevel + 0.01; } // v7.83: Update display using cached DOM reference const cache = this._getStatusBarCache(); if (cache.gridLevel) cache.gridLevel.textContent = this.gridLevel; this.updateStatusBar(); if (typeof AudioSystem !== 'undefined') { AudioSystem.click(); } }, // v7.83: Get cached status bar DOM references _getStatusBarCache() { if (!this._statusBarCache) { this._statusBarCache = { tool: document.getElementById('status-tool'), brush: document.getElementById('status-brush'), symmetry: document.getElementById('status-symmetry'), gridY: document.getElementById('status-grid-y'), blocks: document.getElementById('status-blocks'), gridLevel: document.getElementById('grid-level-display') }; } return this._statusBarCache; }, // Status bar update // v7.83: Uses cached DOM references to eliminate 5 getElementById calls per update updateStatusBar() { const cache = this._getStatusBarCache(); if (cache.tool) { const toolNames = { place: 'Place', fill: 'Fill', line: 'Line', select: 'Select' }; cache.tool.textContent = toolNames[this.currentTool] || this.currentTool; } if (cache.brush) cache.brush.textContent = `${this.brushSize}x${this.brushSize}`; if (cache.symmetry) { const symNames = { none: 'Off', x: 'X', z: 'Z', xz: 'XZ' }; cache.symmetry.textContent = symNames[this.symmetryMode] || 'Off'; } if (cache.gridY) cache.gridY.textContent = this.gridLevel; if (cache.blocks) cache.blocks.textContent = this.placedBlocks.length; }, // Undo/Redo system pushUndoAction(action) { this.undoStack.push(action); if (this.undoStack.length > this.maxUndoSteps) { this.undoStack.shift(); } // Clear redo stack when new action is performed this.redoStack = []; }, undo() { if (this.undoStack.length === 0) { showNotification('Nothing to undo', 'info'); return; } const action = this.undoStack.pop(); this.redoStack.push(action); if (action.type === 'place') { // Remove the placed blocks action.blocks.forEach(blockData => { const idx = this.placedBlocks.findIndex(b => b.posKey === blockData.posKey); if (idx !== -1) { const block = this.placedBlocks[idx]; if (block.mesh && block.mesh.parent) { block.mesh.parent.remove(block.mesh); } this.placedBlocks.splice(idx, 1); const meshIdx = this.blockMeshes.indexOf(block.mesh); if (meshIdx !== -1) this.blockMeshes.splice(meshIdx, 1); } }); } else if (action.type === 'remove') { // Re-add the removed blocks action.blocks.forEach(blockData => { this.placeBlockAt(blockData.x, blockData.y, blockData.z, blockData.type, false); }); } this.updateBlockCount(); showNotification(`Undo: ${action.type} ${action.blocks.length} block(s)`, 'info'); if (typeof AudioSystem !== 'undefined') { AudioSystem.click(); } }, redo() { if (this.redoStack.length === 0) { showNotification('Nothing to redo', 'info'); return; } const action = this.redoStack.pop(); this.undoStack.push(action); if (action.type === 'place') { // Re-place the blocks action.blocks.forEach(blockData => { this.placeBlockAt(blockData.x, blockData.y, blockData.z, blockData.type, false); }); } else if (action.type === 'remove') { // Re-remove the blocks action.blocks.forEach(blockData => { const idx = this.placedBlocks.findIndex(b => b.posKey === blockData.posKey); if (idx !== -1) { const block = this.placedBlocks[idx]; if (block.mesh && block.mesh.parent) { block.mesh.parent.remove(block.mesh); } this.placedBlocks.splice(idx, 1); const meshIdx = this.blockMeshes.indexOf(block.mesh); if (meshIdx !== -1) this.blockMeshes.splice(meshIdx, 1); } }); } this.updateBlockCount(); showNotification(`Redo: ${action.type} ${action.blocks.length} block(s)`, 'info'); if (typeof AudioSystem !== 'undefined') { AudioSystem.click(); } }, // Place block at specific position (helper for undo/redo and prefabs) placeBlockAt(x, y, z, blockTypeId, recordUndo = true) { const posKey = `${x},${y},${z}`; // Check for duplicate if (this.placedBlocks.some(b => b.posKey === posKey)) return null; const blockType = this.blockTypes.find(b => b.id === blockTypeId); if (!blockType) return null; // Create material if not cached if (!this.materialCache[blockTypeId]) { const matOptions = { color: blockType.color, roughness: blockType.roughness, metalness: blockType.metalness }; if (blockType.emissive) { matOptions.emissive = blockType.emissive; matOptions.emissiveIntensity = blockType.emissiveIntensity || 0.3; } if (blockType.transparent) { matOptions.transparent = true; matOptions.opacity = blockType.opacity; } this.materialCache[blockTypeId] = new THREE.MeshStandardMaterial(matOptions); } const material = this.materialCache[blockTypeId].clone(); const mesh = new THREE.Mesh(this.blockGeometry, material); mesh.position.set(x, y, z); mesh.castShadow = true; mesh.receiveShadow = true; mesh.userData.isBuilderBlock = true; mesh.userData.blockType = blockTypeId; // v8.31: Track mesh/material with ResourceManager if (typeof ResourceManager !== 'undefined') { ResourceManager.track(material); ResourceManager.track(mesh); } if (typeof scene !== 'undefined') { scene.add(mesh); } const blockData = { posKey, x, y, z, type: blockTypeId, mesh }; this.blockMeshes.push(mesh); this.placedBlocks.push(blockData); if (recordUndo) { this.pushUndoAction({ type: 'place', blocks: [{ posKey, x, y, z, type: blockTypeId }] }); } return blockData; }, // Copy selection to clipboard copySelection() { if (this.selectionStart && this.selectionEnd) { // Copy blocks within selection box const minX = Math.min(this.selectionStart.x, this.selectionEnd.x); const maxX = Math.max(this.selectionStart.x, this.selectionEnd.x); const minY = Math.min(this.selectionStart.y, this.selectionEnd.y); const maxY = Math.max(this.selectionStart.y, this.selectionEnd.y); const minZ = Math.min(this.selectionStart.z, this.selectionEnd.z); const maxZ = Math.max(this.selectionStart.z, this.selectionEnd.z); this.clipboard = this.placedBlocks.filter(b => b.x >= minX && b.x <= maxX && b.y >= minY && b.y <= maxY && b.z >= minZ && b.z <= maxZ ).map(b => ({ x: b.x - minX, y: b.y - minY, z: b.z - minZ, type: b.type })); showNotification(`Copied ${this.clipboard.length} blocks`, 'info'); } else { showNotification('No selection to copy. Use Select tool first.', 'error'); } if (typeof AudioSystem !== 'undefined') { AudioSystem.click(); } }, // Paste clipboard at ghost position pasteClipboard() { if (this.clipboard.length === 0) { showNotification('Clipboard is empty', 'error'); return; } if (!this.ghostBlock || !this.ghostBlock.visible) { showNotification('Move cursor to paste location', 'error'); return; } const basePos = this.ghostBlock.position; const placedBlocks = []; this.clipboard.forEach(blockData => { const x = basePos.x + blockData.x; const y = basePos.y + blockData.y; const z = basePos.z + blockData.z; const result = this.placeBlockAt(x, y, z, blockData.type, false); if (result) { placedBlocks.push({ posKey: result.posKey, x, y, z, type: blockData.type }); } }); if (placedBlocks.length > 0) { this.pushUndoAction({ type: 'place', blocks: placedBlocks }); } this.updateBlockCount(); showNotification(`Pasted ${placedBlocks.length} blocks`, 'info'); if (typeof AudioSystem !== 'undefined') { AudioSystem.craft(); } }, // Prefab system populatePrefabGrid() { const grid = document.getElementById('prefab-grid'); if (!grid) return; grid.innerHTML = ''; Object.entries(this.prefabs).forEach(([key, prefab]) => { const item = document.createElement('div'); item.className = 'prefab-item'; item.onclick = () => this.placePrefab(key); item.innerHTML = `
${prefab.icon}
${prefab.name}
`; grid.appendChild(item); }); }, placePrefab(prefabKey) { const prefab = this.prefabs[prefabKey]; if (!prefab) return; if (!this.ghostBlock || !this.ghostBlock.visible) { showNotification('Move cursor to placement location', 'error'); return; } const basePos = this.ghostBlock.position; const placedBlocks = []; prefab.blocks.forEach(blockData => { const x = basePos.x + blockData.x; const y = basePos.y + blockData.y; const z = basePos.z + blockData.z; const result = this.placeBlockAt(x, y, z, blockData.t, false); if (result) { placedBlocks.push({ posKey: result.posKey, x, y, z, type: blockData.t }); } }); if (placedBlocks.length > 0) { this.pushUndoAction({ type: 'place', blocks: placedBlocks }); } this.updateBlockCount(); showNotification(`Placed ${prefab.name} (${placedBlocks.length} blocks)`, 'info'); // Close menu this.togglePrefabMenu(); if (typeof AudioSystem !== 'undefined') { AudioSystem.levelUp(); } }, togglePrefabMenu() { const menu = document.getElementById('builder-prefab-menu'); if (menu) { menu.classList.toggle('active'); } }, toggleSettingsMenu() { const menu = document.getElementById('builder-settings-menu'); if (menu) { menu.classList.toggle('active'); } }, // Fill tool - fill region between two points fillRegion(start, end) { const minX = Math.min(start.x, end.x); const maxX = Math.max(start.x, end.x); const minY = Math.min(start.y, end.y); const maxY = Math.max(start.y, end.y); const minZ = Math.min(start.z, end.z); const maxZ = Math.max(start.z, end.z); const blockType = this.blockTypes[this.selectedBlockIndex]; const placedBlocks = []; for (let x = minX; x <= maxX; x += this.gridSize) { for (let y = minY; y <= maxY; y += this.gridSize) { for (let z = minZ; z <= maxZ; z += this.gridSize) { const result = this.placeBlockAt(x, y, z, blockType.id, false); if (result) { placedBlocks.push({ posKey: result.posKey, x, y, z, type: blockType.id }); } } } } if (placedBlocks.length > 0) { this.pushUndoAction({ type: 'place', blocks: placedBlocks }); } this.updateBlockCount(); showNotification(`Filled ${placedBlocks.length} blocks`, 'info'); if (typeof AudioSystem !== 'undefined') { AudioSystem.craft(); } }, // Line tool - draw line between two points drawLine(start, end) { const blockType = this.blockTypes[this.selectedBlockIndex]; const placedBlocks = []; // Bresenham's line algorithm in 3D const dx = Math.abs(end.x - start.x); const dy = Math.abs(end.y - start.y); const dz = Math.abs(end.z - start.z); const sx = start.x < end.x ? this.gridSize : -this.gridSize; const sy = start.y < end.y ? this.gridSize : -this.gridSize; const sz = start.z < end.z ? this.gridSize : -this.gridSize; const dm = Math.max(dx, dy, dz); let x = start.x, y = start.y, z = start.z; for (let i = 0; i <= dm / this.gridSize; i++) { const result = this.placeBlockAt( Math.round(x / this.gridSize) * this.gridSize, Math.round(y / this.gridSize) * this.gridSize, Math.round(z / this.gridSize) * this.gridSize, blockType.id, false ); if (result) { placedBlocks.push({ posKey: result.posKey, x: result.x, y: result.y, z: result.z, type: blockType.id }); } x += (dx / dm) * sx; y += (dy / dm) * sy; z += (dz / dm) * sz; } if (placedBlocks.length > 0) { this.pushUndoAction({ type: 'place', blocks: placedBlocks }); } this.updateBlockCount(); showNotification(`Drew line with ${placedBlocks.length} blocks`, 'info'); if (typeof AudioSystem !== 'undefined') { AudioSystem.craft(); } }, // Update ghost block material to match selected updateGhostMaterial() { if (!this.ghostBlock) return; const block = this.blockTypes[this.selectedBlockIndex]; const mat = this.ghostBlock.material; mat.color.setHex(block.color); mat.emissive.setHex(block.emissive || 0x004444); mat.opacity = 0.5; mat.needsUpdate = true; }, // Toggle builder mode toggle() { this.active = !this.active; const panel = document.getElementById('builder-panel'); const toggleBtn = document.getElementById('builder-toggle-btn'); const countDisplay = document.getElementById('builder-block-count'); if (this.active) { panel?.classList.add('active'); toggleBtn?.classList.add('active'); if (countDisplay) countDisplay.style.display = 'block'; // Add ghost block to scene if (this.ghostBlock && typeof scene !== 'undefined') { scene.add(this.ghostBlock); this.ghostBlock.visible = true; } // Create grid helper this.showGrid(); this.updateBlockCount(); showNotification('🧱 Builder Mode ON - Click to place blocks', 'info'); // Play activation sound if (typeof AudioSystem !== 'undefined') { AudioSystem.levelUp(); } } else { panel?.classList.remove('active'); toggleBtn?.classList.remove('active'); if (countDisplay) countDisplay.style.display = 'none'; // Remove ghost block if (this.ghostBlock) { this.ghostBlock.visible = false; if (this.ghostBlock.parent) { this.ghostBlock.parent.remove(this.ghostBlock); } } // Remove grid this.hideGrid(); showNotification('🧱 Builder Mode OFF', 'info'); } return this.active; }, // Show placement grid on ground showGrid() { if (this.gridHelper) return; const gridSize = 100; const divisions = gridSize / this.gridSize; this.gridHelper = new THREE.GridHelper(gridSize, divisions, 0x00ffff, 0x004444); this.gridHelper.material.opacity = 0.3; this.gridHelper.material.transparent = true; // Position grid at player's level if (typeof worldState !== 'undefined' && worldState.player) { const playerY = Math.floor(worldState.player.position.y); this.gridHelper.position.y = playerY + 0.01; } if (typeof scene !== 'undefined') { scene.add(this.gridHelper); } }, hideGrid() { if (this.gridHelper) { if (this.gridHelper.parent) { this.gridHelper.parent.remove(this.gridHelper); } this.gridHelper.geometry.dispose(); this.gridHelper.material.dispose(); this.gridHelper = null; } }, // Snap position to grid // v7.83: Uses pre-allocated _tempSnapped vector to avoid GC pressure snapToGrid(pos) { if (!this._tempSnapped) this._tempSnapped = new THREE.Vector3(); return this._tempSnapped.set( Math.round(pos.x / this.gridSize) * this.gridSize, Math.round(pos.y / this.gridSize) * this.gridSize, Math.round(pos.z / this.gridSize) * this.gridSize ); }, // Update ghost block position based on mouse/raycaster // v7.83: Uses pre-allocated vectors for ground plane and intersection updateGhostPosition(raycaster) { if (!this.active || !this.ghostBlock) return; // Raycast against ground and existing blocks const targets = [ ...(typeof worldState !== 'undefined' && worldState.terrain ? worldState.terrain : []), ...this.blockMeshes ]; // v7.83: Use pre-allocated ground plane and intersection vector if (!this._tempGroundPlane) this._tempGroundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); if (!this._tempGroundIntersect) this._tempGroundIntersect = new THREE.Vector3(); raycaster.ray.intersectPlane(this._tempGroundPlane, this._tempGroundIntersect); let targetPos = null; // v7.83: Reuse pre-allocated normal vector if (!this._tempNormal) this._tempNormal = new THREE.Vector3(0, 1, 0); this._tempNormal.set(0, 1, 0); // Check block intersections first const blockHits = raycaster.intersectObjects(this.blockMeshes, false); if (blockHits.length > 0) { const hit = blockHits[0]; targetPos = hit.point.clone(); this._tempNormal.copy(hit.face.normal); // Place on the face that was hit targetPos.add(this._tempNormal.multiplyScalar(this.gridSize * 0.5)); } else if (this._tempGroundIntersect && this._tempGroundIntersect.length() < 500) { targetPos = this._tempGroundIntersect.clone(); targetPos.y = Math.max(0, targetPos.y) + this.gridSize * 0.5; } if (targetPos) { const snapped = this.snapToGrid(targetPos); this.ghostBlock.position.copy(snapped); this.ghostBlock.visible = true; } else { this.ghostBlock.visible = false; } }, // Place a block at ghost position (enhanced with brush size and symmetry) placeBlock() { if (!this.active || !this.ghostBlock || !this.ghostBlock.visible) return false; const basePos = this.ghostBlock.position.clone(); const blockType = this.blockTypes[this.selectedBlockIndex]; const placedBlocks = []; // Generate all positions based on brush size const brushPositions = []; for (let dx = 0; dx < this.brushSize; dx++) { for (let dz = 0; dz < this.brushSize; dz++) { const pos = basePos.clone(); pos.x += dx * this.gridSize; pos.z += dz * this.gridSize; brushPositions.push(pos); } } // For each brush position, also add symmetry positions const allPositions = []; brushPositions.forEach(pos => { const symPositions = this.getSymmetryPositions(pos); symPositions.forEach(sp => { // Avoid duplicate positions if (!allPositions.some(p => p.x === sp.x && p.y === sp.y && p.z === sp.z)) { allPositions.push(sp); } }); }); // Place blocks at all positions allPositions.forEach(pos => { const posKey = `${pos.x},${pos.y},${pos.z}`; // Skip if duplicate or last placement if (this.lastPlacementPos === posKey) return; if (this.placedBlocks.some(b => b.posKey === posKey)) return; // Check height limit if (pos.y > this.maxHeight) return; // Create block mesh const material = this.materialCache[blockType.id].clone(); const mesh = new THREE.Mesh(this.blockGeometry, material); mesh.position.copy(pos); mesh.castShadow = true; mesh.receiveShadow = true; mesh.userData.isBuilderBlock = true; mesh.userData.blockType = blockType.id; // v8.31: Track mesh/material with ResourceManager if (typeof ResourceManager !== 'undefined') { ResourceManager.track(material); ResourceManager.track(mesh); } // Add to scene if (typeof scene !== 'undefined') { scene.add(mesh); } // Track block this.blockMeshes.push(mesh); this.placedBlocks.push({ posKey: posKey, x: pos.x, y: pos.y, z: pos.z, type: blockType.id, mesh: mesh }); placedBlocks.push({ posKey, x: pos.x, y: pos.y, z: pos.z, type: blockType.id }); }); if (placedBlocks.length === 0) return false; // Record undo action this.pushUndoAction({ type: 'place', blocks: placedBlocks }); // Set last placement to prevent rapid duplicates this.lastPlacementPos = `${basePos.x},${basePos.y},${basePos.z}`; setTimeout(() => { if (this.lastPlacementPos === `${basePos.x},${basePos.y},${basePos.z}`) { this.lastPlacementPos = null; } }, 50); this.updateBlockCount(); // Play placement sound if (typeof AudioSystem !== 'undefined') { AudioSystem.craft(); } return true; }, // Remove block at position removeBlock(raycaster) { if (!this.active) return false; const hits = raycaster.intersectObjects(this.blockMeshes, false); if (hits.length === 0) return false; const hit = hits[0]; const mesh = hit.object; // Find and remove from tracking const idx = this.blockMeshes.indexOf(mesh); if (idx !== -1) { this.blockMeshes.splice(idx, 1); } const blockIdx = this.placedBlocks.findIndex(b => b.mesh === mesh); if (blockIdx !== -1) { this.placedBlocks.splice(blockIdx, 1); } // Remove from scene if (mesh.parent) { mesh.parent.remove(mesh); } mesh.geometry.dispose(); mesh.material.dispose(); this.updateBlockCount(); // Play removal sound if (typeof AudioSystem !== 'undefined') { AudioSystem.hit(); } return true; }, // Update block count display updateBlockCount() { const countEl = document.getElementById('block-count-num'); if (countEl) { countEl.textContent = this.placedBlocks.length; } }, // Save structures to localStorage for current planet saveStructures() { if (typeof activeCiv === 'undefined' || !activeCiv) return; const planetId = activeCiv.id || activeCiv.name; const saveKey = `leviathan_builder_${planetId}`; const saveData = this.placedBlocks.map(b => ({ x: b.x, y: b.y, z: b.z, type: b.type })); localStorage.setItem(saveKey, JSON.stringify(saveData)); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`🧱 Saved ${saveData.length} blocks for planet ${planetId}`); }, // Load structures from localStorage for current planet loadStructures() { if (typeof activeCiv === 'undefined' || !activeCiv) return; const planetId = activeCiv.id || activeCiv.name; const saveKey = `leviathan_builder_${planetId}`; // v8.0: Using SafeJSON for builder blocks (8-Strategy Consensus Cycle 7) const blocks = SafeJSON.fromLocalStorage(saveKey, null); if (!blocks) return; // Clear existing blocks first this.clearAllBlocks(); // Rebuild all blocks blocks.forEach(blockData => { const blockType = this.blockTypes.find(b => b.id === blockData.type); if (!blockType) return; const material = this.materialCache[blockData.type].clone(); const mesh = new THREE.Mesh(this.blockGeometry, material); mesh.position.set(blockData.x, blockData.y, blockData.z); mesh.castShadow = true; mesh.receiveShadow = true; mesh.userData.isBuilderBlock = true; mesh.userData.blockType = blockData.type; if (typeof scene !== 'undefined') { scene.add(mesh); } this.blockMeshes.push(mesh); this.placedBlocks.push({ posKey: `${blockData.x},${blockData.y},${blockData.z}`, x: blockData.x, y: blockData.y, z: blockData.z, type: blockData.type, mesh: mesh }); }); this.updateBlockCount(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`🧱 Loaded ${blocks.length} blocks for planet ${planetId}`); }, // Clear all placed blocks clearAllBlocks() { this.blockMeshes.forEach(mesh => { if (mesh.parent) mesh.parent.remove(mesh); mesh.geometry.dispose(); mesh.material.dispose(); }); this.blockMeshes = []; this.placedBlocks = []; this.updateBlockCount(); }, // Show toggle button when in world mode (unless unified HUD is enabled) showToggleButton() { // v10.32: Don't show if unified HUD is enabled (it will handle builder via quick buttons) if (typeof UnifiedHUD !== 'undefined' && UnifiedHUD.enabled) return; const btn = document.getElementById('builder-toggle-btn'); if (btn) btn.style.display = 'block'; }, hideToggleButton() { const btn = document.getElementById('builder-toggle-btn'); if (btn) btn.style.display = 'none'; // Also deactivate builder mode if (this.active) { this.toggle(); } }, // Handle scroll wheel for block selection handleScroll(deltaY) { if (!this.active) return; const direction = deltaY > 0 ? 1 : -1; let newIndex = this.selectedBlockIndex + direction; if (newIndex < 0) newIndex = this.blockTypes.length - 1; if (newIndex >= this.blockTypes.length) newIndex = 0; // Update page if needed const newPage = Math.floor(newIndex / this.blocksPerPage); if (newPage !== this.currentPage) { this.currentPage = newPage; } this.selectBlock(newIndex); }, // Export current structure as JSON exportStructure() { const data = { name: 'My Structure', created: new Date().toISOString(), blocks: this.placedBlocks.map(b => ({ x: b.x, y: b.y, z: b.z, type: b.type })) }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `structure-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); showNotification(`📦 Exported ${data.blocks.length} blocks`, 'info'); }, // Import structure from JSON // v8.31: Use ErrorRecovery.safeJSONParse for safer import importStructure(file) { const reader = new FileReader(); reader.onload = (e) => { try { const data = ErrorRecovery.safeJSONParse(e.target.result, null); if (!data) { showNotification('Failed to parse structure file', 'error'); return; } if (data.blocks && Array.isArray(data.blocks)) { // Find center offset let minX = Infinity, minZ = Infinity, minY = Infinity; data.blocks.forEach(b => { minX = Math.min(minX, b.x); minY = Math.min(minY, b.y); minZ = Math.min(minZ, b.z); }); // Place relative to player const playerPos = worldState?.player?.position || { x: 0, y: 0, z: 0 }; data.blocks.forEach(blockData => { const blockType = this.blockTypes.find(bt => bt.id === blockData.type); if (!blockType) return; const pos = new THREE.Vector3( blockData.x - minX + playerPos.x + 5, blockData.y - minY + 1, blockData.z - minZ + playerPos.z + 5 ); const snapped = this.snapToGrid(pos); const posKey = `${snapped.x},${snapped.y},${snapped.z}`; // Skip duplicates if (this.placedBlocks.some(b => b.posKey === posKey)) return; const material = this.materialCache[blockData.type].clone(); const mesh = new THREE.Mesh(this.blockGeometry, material); mesh.position.copy(snapped); mesh.castShadow = true; mesh.receiveShadow = true; mesh.userData.isBuilderBlock = true; mesh.userData.blockType = blockData.type; // v8.31: Track mesh/material with ResourceManager if (typeof ResourceManager !== 'undefined') { ResourceManager.track(material); ResourceManager.track(mesh); } if (typeof scene !== 'undefined') { scene.add(mesh); } this.blockMeshes.push(mesh); this.placedBlocks.push({ posKey: posKey, x: snapped.x, y: snapped.y, z: snapped.z, type: blockData.type, mesh: mesh }); }); this.updateBlockCount(); showNotification(`📦 Imported ${data.blocks.length} blocks`, 'info'); } } catch (err) { showNotification('Failed to import structure', 'error'); console.error(err); } }; reader.readAsText(file); } }; // Initialize builder mode BuilderMode.init(); window.BuilderMode = BuilderMode; // Builder mode keyboard shortcuts document.addEventListener('keydown', (e) => { // Don't trigger when typing in inputs if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (typeof mode === 'undefined' || mode !== 'world') return; // B key - toggle builder mode if (e.key === 'b' || e.key === 'B') { BuilderMode.toggle(); e.preventDefault(); } // Number keys 1-9 for block selection (when builder is active) if (BuilderMode.active && e.key >= '1' && e.key <= '9') { const slotNum = parseInt(e.key) - 1; const blockIndex = BuilderMode.currentPage * BuilderMode.blocksPerPage + slotNum; if (blockIndex < BuilderMode.blockTypes.length) { BuilderMode.selectBlock(blockIndex); } e.preventDefault(); } // 0 key for 10th slot if (BuilderMode.active && e.key === '0') { const slotNum = 9; const blockIndex = BuilderMode.currentPage * BuilderMode.blocksPerPage + slotNum; if (blockIndex < BuilderMode.blockTypes.length) { BuilderMode.selectBlock(blockIndex); } e.preventDefault(); } // Q/E for page navigation in builder mode if (BuilderMode.active) { if (e.key === 'q' || e.key === 'Q') { BuilderMode.prevPage(); e.preventDefault(); } if (e.key === 'e' || e.key === 'E') { BuilderMode.nextPage(); e.preventDefault(); } } // Escape to exit builder mode if (BuilderMode.active && e.key === 'Escape') { BuilderMode.toggle(); e.preventDefault(); } }); // Builder mode scroll wheel handler document.addEventListener('wheel', (e) => { if (BuilderMode.active && typeof mode !== 'undefined' && mode === 'world') { BuilderMode.handleScroll(e.deltaY); e.preventDefault(); } }, { passive: false }); // Builder mode mouse move handler - update ghost block position document.addEventListener('mousemove', (e) => { if (!BuilderMode.active || typeof mode === 'undefined' || mode !== 'world') return; if (typeof mouse === 'undefined' || typeof raycaster === 'undefined' || typeof camera === 'undefined') return; // Update mouse coordinates mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; // Update raycaster and ghost position raycaster.setFromCamera(mouse, camera); BuilderMode.updateGhostPosition(raycaster); }); // Builder mode click handler - place or remove blocks document.addEventListener('mousedown', (e) => { if (!BuilderMode.active || typeof mode === 'undefined' || mode !== 'world') return; // Ignore if clicking on UI elements if (e.target.closest('#builder-panel, #builder-toggle-btn, .modal, .menu, button, input, select')) return; // Only handle left and right click if (e.button !== 0 && e.button !== 2) return; // Update mouse and raycaster if (typeof mouse !== 'undefined' && typeof raycaster !== 'undefined' && typeof camera !== 'undefined') { mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); // Shift+Click or Right-click = remove block if (e.shiftKey || e.button === 2) { if (BuilderMode.removeBlock(raycaster)) { e.preventDefault(); e.stopPropagation(); } } // Normal click = place block else if (e.button === 0) { if (BuilderMode.placeBlock()) { e.preventDefault(); e.stopPropagation(); } } } }); // Prevent context menu in builder mode document.addEventListener('contextmenu', (e) => { if (BuilderMode.active && typeof mode !== 'undefined' && mode === 'world') { e.preventDefault(); } }); // ═══════════════════════════════════════════════════════════════════════════════════════ // v12.14: BOOK FACTORY - VIRTUAL TWIN SIMULATION // Digital twin of autonomous book manufacturing process // Users observe robotic workers completing the full production cycle // Validates process completion for training and verification // ═══════════════════════════════════════════════════════════════════════════════════════ const BookFactory = { // State active: false, sceneGroup: null, booksInProgress: [], completedBooks: [], totalBooksProduced: 0, currentBatch: 0, batchSize: 5, isPaused: false, jobConfirmed: false, // v7.72: Cached UI element references _uiCache: null, // Process timing (seconds per stage) stageTiming: { RAW_MATERIALS: 2, PAPER_CUTTING: 3, PRINTING: 4, DRYING: 2, COLLATING: 3, BINDING: 4, COVER_APPLICATION: 3, TRIMMING: 2, QUALITY_CHECK: 3, PACKAGING: 2, SHIPPING: 1 }, // Process stages in order stages: [ { id: 'RAW_MATERIALS', name: 'Raw Materials', icon: '📦', color: 0x8B4513, description: 'Paper rolls and ink cartridges loaded' }, { id: 'PAPER_CUTTING', name: 'Paper Cutting', icon: '✂️', color: 0xcccccc, description: 'Cutting sheets from paper rolls' }, { id: 'PRINTING', name: 'Printing', icon: '🖨️', color: 0x4488ff, description: 'Printing content onto pages' }, { id: 'DRYING', name: 'Drying', icon: '💨', color: 0xffaa00, description: 'Ink drying and curing' }, { id: 'COLLATING', name: 'Collating', icon: '📑', color: 0x88ff88, description: 'Assembling pages in order' }, { id: 'BINDING', name: 'Binding', icon: '📚', color: 0xff6644, description: 'Binding pages together' }, { id: 'COVER_APPLICATION', name: 'Cover', icon: '📕', color: 0xaa4444, description: 'Attaching book cover' }, { id: 'TRIMMING', name: 'Trimming', icon: '📐', color: 0x666666, description: 'Final edge trimming' }, { id: 'QUALITY_CHECK', name: 'QC Check', icon: '✅', color: 0x44ff44, description: 'Quality inspection' }, { id: 'PACKAGING', name: 'Packaging', icon: '📦', color: 0xddaa77, description: 'Boxing for shipment' }, { id: 'SHIPPING', name: 'Shipping', icon: '🚚', color: 0x44aaff, description: 'Ready for delivery' } ], // 3D objects conveyorBelt: null, stations: [], robotArms: [], bookMeshes: [], factoryFloor: null, // Animation conveyorSpeed: 0.5, animationTime: 0, // Initialize the factory system init() { this.createUI(); Logger.info('BookFactory', 'Virtual Twin initialized'); }, // Create factory control UI createUI() { const panel = document.createElement('div'); panel.id = 'factory-panel'; panel.innerHTML = `
🏭

BOOK FACTORY

Virtual Twin Simulation
RUNNING
0
Books Produced
0
In Progress
1
Batch #
0%
Batch Progress
✅ BATCH COMPLETE
All 5 books have been successfully manufactured.
Process completed autonomously.
`; document.body.appendChild(panel); // Populate stages this.updateStagesList(); }, // Update stages list in UI updateStagesList() { const container = document.getElementById('process-stages'); if (!container) return; container.innerHTML = this.stages.map((stage, idx) => `
${stage.icon}
${stage.name}
${stage.description}
0
`).join(''); }, // Build the 3D factory environment buildFactory(scene) { this.sceneGroup = new THREE.Group(); this.sceneGroup.name = 'BookFactory'; // Factory floor // v8.29: Track geometry/materials with ResourceManager const floorGeo = new THREE.PlaneGeometry(60, 40); const floorMat = new THREE.MeshStandardMaterial({ color: 0x333344, roughness: 0.8, metalness: 0.2 }); if (typeof ResourceManager !== 'undefined') { ResourceManager.track(floorGeo); ResourceManager.track(floorMat); } this.factoryFloor = new THREE.Mesh(floorGeo, floorMat); this.factoryFloor.rotation.x = -Math.PI / 2; this.factoryFloor.position.y = 0; this.factoryFloor.receiveShadow = true; this.sceneGroup.add(this.factoryFloor); // Floor grid lines const gridHelper = new THREE.GridHelper(60, 30, 0x444466, 0x333355); gridHelper.position.y = 0.01; this.sceneGroup.add(gridHelper); // Build conveyor belt system this.buildConveyorBelt(); // Build stations along the conveyor this.buildStations(); // Build robot arms at each station this.buildRobotArms(); // Add factory walls/backdrop this.buildFactoryStructure(); // Add lighting this.addFactoryLighting(); scene.add(this.sceneGroup); Logger.info('Factory3D', 'Factory 3D environment built'); }, // Build the main conveyor belt buildConveyorBelt() { const conveyorGroup = new THREE.Group(); const beltLength = 50; const beltWidth = 2; // Main belt surface // v8.29: Track geometry/materials with ResourceManager const beltGeo = new THREE.BoxGeometry(beltLength, 0.3, beltWidth); const beltMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.9, metalness: 0.1 }); if (typeof ResourceManager !== 'undefined') { ResourceManager.track(beltGeo); ResourceManager.track(beltMat); } const belt = new THREE.Mesh(beltGeo, beltMat); belt.position.y = 1; belt.receiveShadow = true; conveyorGroup.add(belt); // Belt side rails // v8.29: Track geometry/materials with ResourceManager const railGeo = new THREE.BoxGeometry(beltLength, 0.5, 0.1); const railMat = new THREE.MeshStandardMaterial({ color: 0x666677, roughness: 0.5, metalness: 0.5 }); if (typeof ResourceManager !== 'undefined') { ResourceManager.track(railGeo); ResourceManager.track(railMat); } const railLeft = new THREE.Mesh(railGeo, railMat); railLeft.position.set(0, 1.15, beltWidth / 2 + 0.05); conveyorGroup.add(railLeft); const railRight = new THREE.Mesh(railGeo, railMat); railRight.position.set(0, 1.15, -beltWidth / 2 - 0.05); conveyorGroup.add(railRight); // Belt legs/supports const legGeo = new THREE.BoxGeometry(0.3, 1, 0.3); const legMat = new THREE.MeshStandardMaterial({ color: 0x555566, roughness: 0.7, metalness: 0.3 }); for (let i = -20; i <= 20; i += 5) { const legL = new THREE.Mesh(legGeo, legMat); legL.position.set(i, 0.5, beltWidth / 2 + 0.3); legL.castShadow = true; conveyorGroup.add(legL); const legR = new THREE.Mesh(legGeo, legMat); legR.position.set(i, 0.5, -beltWidth / 2 - 0.3); legR.castShadow = true; conveyorGroup.add(legR); } // Animated belt texture strips (visual movement) const stripGeo = new THREE.BoxGeometry(0.5, 0.05, beltWidth - 0.1); const stripMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.95 }); this.beltStrips = []; for (let i = -24; i <= 24; i += 1.5) { const strip = new THREE.Mesh(stripGeo, stripMat); strip.position.set(i, 1.18, 0); conveyorGroup.add(strip); this.beltStrips.push(strip); } this.conveyorBelt = conveyorGroup; this.sceneGroup.add(conveyorGroup); }, // Build processing stations buildStations() { this.stations = []; const stationSpacing = 4.5; const startX = -22; this.stages.forEach((stage, idx) => { const stationGroup = new THREE.Group(); stationGroup.name = `Station_${stage.id}`; const x = startX + idx * stationSpacing; // Station platform const platformGeo = new THREE.BoxGeometry(3, 0.2, 4); const platformMat = new THREE.MeshStandardMaterial({ color: stage.color, roughness: 0.6, metalness: 0.3 }); const platform = new THREE.Mesh(platformGeo, platformMat); platform.position.set(0, 0.1, -3); platform.receiveShadow = true; stationGroup.add(platform); // Station equipment (varies by type) const equipmentGroup = this.createStationEquipment(stage); equipmentGroup.position.set(0, 0.2, -3); stationGroup.add(equipmentGroup); // Station sign const signGeo = new THREE.BoxGeometry(2.5, 0.8, 0.1); const signMat = new THREE.MeshStandardMaterial({ color: 0x222233, roughness: 0.5, emissive: stage.color, emissiveIntensity: 0.2 }); const sign = new THREE.Mesh(signGeo, signMat); sign.position.set(0, 3.5, -4); stationGroup.add(sign); // Status light on station const lightGeo = new THREE.SphereGeometry(0.15, 16, 16); const lightMat = new THREE.MeshStandardMaterial({ color: 0x00ff00, emissive: 0x00ff00, emissiveIntensity: 0.5 }); const statusLight = new THREE.Mesh(lightGeo, lightMat); statusLight.position.set(1.5, 3.5, -4); stationGroup.add(statusLight); stationGroup.position.x = x; stationGroup.userData = { stage: stage, statusLight: statusLight, idx: idx }; this.stations.push(stationGroup); this.sceneGroup.add(stationGroup); }); }, // Create equipment specific to each station type createStationEquipment(stage) { const group = new THREE.Group(); const metalMat = new THREE.MeshStandardMaterial({ color: 0x888899, roughness: 0.4, metalness: 0.7 }); switch (stage.id) { case 'RAW_MATERIALS': // Paper roll dispenser const rollGeo = new THREE.CylinderGeometry(0.4, 0.4, 1.5, 16); const rollMat = new THREE.MeshStandardMaterial({ color: 0xffeedd, roughness: 0.9 }); for (let i = 0; i < 3; i++) { const roll = new THREE.Mesh(rollGeo, rollMat); roll.rotation.z = Math.PI / 2; roll.position.set(-0.5 + i * 0.5, 0.8, 0); group.add(roll); } break; case 'PAPER_CUTTING': // Cutting blade const bladeGeo = new THREE.BoxGeometry(0.05, 1.2, 2); const bladeMat = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.9 }); const blade = new THREE.Mesh(bladeGeo, bladeMat); blade.position.y = 1; group.add(blade); group.userData.blade = blade; break; case 'PRINTING': // Printer head const headGeo = new THREE.BoxGeometry(1.5, 0.3, 0.5); const head = new THREE.Mesh(headGeo, metalMat); head.position.y = 1.2; group.add(head); group.userData.printHead = head; // Ink cartridges const cartGeo = new THREE.BoxGeometry(0.2, 0.4, 0.2); const colors = [0x00ffff, 0xff00ff, 0xffff00, 0x000000]; colors.forEach((c, i) => { const cart = new THREE.Mesh(cartGeo, new THREE.MeshStandardMaterial({ color: c })); cart.position.set(-0.4 + i * 0.3, 1.5, -0.5); group.add(cart); }); break; case 'DRYING': // Heat lamps const lampGeo = new THREE.CylinderGeometry(0.3, 0.4, 0.2, 16); const lampMat = new THREE.MeshStandardMaterial({ color: 0xff4400, emissive: 0xff2200, emissiveIntensity: 0.5 }); for (let i = 0; i < 3; i++) { const lamp = new THREE.Mesh(lampGeo, lampMat); lamp.position.set(-0.6 + i * 0.6, 1.5, 0); group.add(lamp); } break; case 'COLLATING': // Stacking mechanism const stackerGeo = new THREE.BoxGeometry(1, 1.5, 1); const stacker = new THREE.Mesh(stackerGeo, metalMat); stacker.position.y = 0.75; group.add(stacker); break; case 'BINDING': // Binding press const pressGeo = new THREE.BoxGeometry(1.2, 0.3, 1.2); const press = new THREE.Mesh(pressGeo, metalMat); press.position.y = 1.2; group.add(press); group.userData.press = press; const baseGeo = new THREE.BoxGeometry(1.4, 0.5, 1.4); const base = new THREE.Mesh(baseGeo, metalMat); base.position.y = 0.25; group.add(base); break; case 'COVER_APPLICATION': // Cover feeder const feederGeo = new THREE.BoxGeometry(1.5, 0.8, 0.3); const feeder = new THREE.Mesh(feederGeo, new THREE.MeshStandardMaterial({ color: 0xaa4444, roughness: 0.7 })); feeder.position.set(0, 1, -0.5); group.add(feeder); break; case 'TRIMMING': // Trimmer blades const trimGeo = new THREE.BoxGeometry(0.03, 0.8, 1.5); const trimMat = new THREE.MeshStandardMaterial({ color: 0xdddddd, metalness: 0.95 }); const trimL = new THREE.Mesh(trimGeo, trimMat); trimL.position.set(-0.5, 0.8, 0); group.add(trimL); const trimR = new THREE.Mesh(trimGeo, trimMat); trimR.position.set(0.5, 0.8, 0); group.add(trimR); break; case 'QUALITY_CHECK': // Camera/scanner const camGeo = new THREE.BoxGeometry(0.4, 0.4, 0.6); const camMat = new THREE.MeshStandardMaterial({ color: 0x222222, emissive: 0x00ff00, emissiveIntensity: 0.3 }); const cam = new THREE.Mesh(camGeo, camMat); cam.position.set(0, 1.5, 0); group.add(cam); // Lens const lensGeo = new THREE.CylinderGeometry(0.1, 0.1, 0.1, 16); const lensMat = new THREE.MeshStandardMaterial({ color: 0x4444ff, emissive: 0x0000ff, emissiveIntensity: 0.5 }); const lens = new THREE.Mesh(lensGeo, lensMat); lens.rotation.x = Math.PI / 2; lens.position.set(0, 1.5, 0.35); group.add(lens); break; case 'PACKAGING': // Box folder const boxerGeo = new THREE.BoxGeometry(1.5, 1, 1.5); const boxer = new THREE.Mesh(boxerGeo, metalMat); boxer.position.y = 0.5; group.add(boxer); break; case 'SHIPPING': // Output chute const chuteGeo = new THREE.BoxGeometry(1.5, 0.1, 2); const chuteMat = new THREE.MeshStandardMaterial({ color: 0x44aaff, roughness: 0.3 }); const chute = new THREE.Mesh(chuteGeo, chuteMat); chute.rotation.x = -0.3; chute.position.set(0, 0.5, 1); group.add(chute); break; } return group; }, // Build robot arms at each station buildRobotArms() { this.robotArms = []; this.stations.forEach((station, idx) => { const armGroup = new THREE.Group(); // Base const baseGeo = new THREE.CylinderGeometry(0.3, 0.4, 0.3, 16); const armMat = new THREE.MeshStandardMaterial({ color: 0xff6600, roughness: 0.5, metalness: 0.6 }); const base = new THREE.Mesh(baseGeo, armMat); armGroup.add(base); // Lower arm const lowerArmGeo = new THREE.BoxGeometry(0.15, 1, 0.15); const lowerArm = new THREE.Mesh(lowerArmGeo, armMat); lowerArm.position.y = 0.65; armGroup.add(lowerArm); // Joint const jointGeo = new THREE.SphereGeometry(0.12, 16, 16); const jointMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.8 }); const joint = new THREE.Mesh(jointGeo, jointMat); joint.position.y = 1.2; armGroup.add(joint); // Upper arm const upperArmGroup = new THREE.Group(); const upperArmGeo = new THREE.BoxGeometry(0.12, 0.8, 0.12); const upperArm = new THREE.Mesh(upperArmGeo, armMat); upperArm.position.y = 0.4; upperArmGroup.add(upperArm); // Gripper const gripperGeo = new THREE.BoxGeometry(0.3, 0.15, 0.1); const gripperMat = new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.7 }); const gripper = new THREE.Mesh(gripperGeo, gripperMat); gripper.position.y = 0.85; upperArmGroup.add(gripper); upperArmGroup.position.y = 1.2; armGroup.add(upperArmGroup); // Position arm next to station armGroup.position.set(station.position.x, 0, -1.5); armGroup.userData = { upperArm: upperArmGroup, stationIdx: idx, animPhase: 0 }; this.robotArms.push(armGroup); this.sceneGroup.add(armGroup); }); }, // Build factory structure (walls, ceiling) buildFactoryStructure() { // Back wall const wallGeo = new THREE.BoxGeometry(65, 8, 0.5); const wallMat = new THREE.MeshStandardMaterial({ color: 0x334455, roughness: 0.9 }); const backWall = new THREE.Mesh(wallGeo, wallMat); backWall.position.set(0, 4, -8); this.sceneGroup.add(backWall); // Side walls const sideWallGeo = new THREE.BoxGeometry(0.5, 8, 16); const leftWall = new THREE.Mesh(sideWallGeo, wallMat); leftWall.position.set(-32, 4, 0); this.sceneGroup.add(leftWall); const rightWall = new THREE.Mesh(sideWallGeo, wallMat); rightWall.position.set(32, 4, 0); this.sceneGroup.add(rightWall); // Ceiling beams const beamGeo = new THREE.BoxGeometry(65, 0.5, 0.5); const beamMat = new THREE.MeshStandardMaterial({ color: 0x555566, metalness: 0.5 }); for (let z = -6; z <= 6; z += 4) { const beam = new THREE.Mesh(beamGeo, beamMat); beam.position.set(0, 7, z); this.sceneGroup.add(beam); } // Factory sign const signGeo = new THREE.BoxGeometry(15, 2, 0.2); const signMat = new THREE.MeshStandardMaterial({ color: 0x001133, emissive: 0x0066aa, emissiveIntensity: 0.5 }); const factorySign = new THREE.Mesh(signGeo, signMat); factorySign.position.set(0, 6.5, -7.5); this.sceneGroup.add(factorySign); }, // Add factory lighting addFactoryLighting() { // Overhead lights const lightGeo = new THREE.BoxGeometry(2, 0.2, 0.5); const lightMat = new THREE.MeshStandardMaterial({ color: 0xffffee, emissive: 0xffffee, emissiveIntensity: 1 }); for (let x = -25; x <= 25; x += 10) { const light = new THREE.Mesh(lightGeo, lightMat); light.position.set(x, 6.5, 0); this.sceneGroup.add(light); // Actual point light const pointLight = new THREE.PointLight(0xffffee, 0.5, 15); pointLight.position.set(x, 5, 0); this.sceneGroup.add(pointLight); } }, // Start the factory simulation start() { this.active = true; this.isPaused = false; this.jobConfirmed = false; this.currentBatch++; this.booksInProgress = []; this.completedBooks = []; // Show UI const panel = document.getElementById('factory-panel'); if (panel) panel.classList.add('active'); // Initialize minimap this.initMinimap(); // Start spawning books this.spawnBook(); this.updateUI(); Logger.info('Factory3D', `Factory simulation started - Batch #${this.currentBatch}`); }, // Stop the factory stop() { this.active = false; const panel = document.getElementById('factory-panel'); if (panel) panel.classList.remove('active'); // Clear books this.bookMeshes.forEach(mesh => { if (mesh.parent) mesh.parent.remove(mesh); }); this.bookMeshes = []; this.booksInProgress = []; }, // Initialize minimap with stations initMinimap() { const minimap = document.getElementById('factory-minimap'); if (!minimap) return; // Clear existing minimap.querySelectorAll('.minimap-station, .minimap-book').forEach(el => el.remove()); // Add stations this.stages.forEach((stage, idx) => { const station = document.createElement('div'); station.className = 'minimap-station'; station.id = `minimap-station-${stage.id}`; station.textContent = stage.icon; station.style.left = `${10 + idx * 8}%`; station.style.top = '50%'; station.style.transform = 'translate(-50%, -50%)'; station.style.backgroundColor = `#${stage.color.toString(16).padStart(6, '0')}`; minimap.appendChild(station); }); }, // Spawn a new book at the start of the line spawnBook() { if (!this.active || this.isPaused) return; if (this.booksInProgress.length >= this.batchSize) return; const book = { id: Date.now() + Math.random(), stageIndex: 0, stageProgress: 0, x: -24, mesh: null }; // Create 3D book mesh const bookGeo = new THREE.BoxGeometry(0.6, 0.1, 0.8); const bookMat = new THREE.MeshStandardMaterial({ color: 0xffeedd, roughness: 0.8 }); const bookMesh = new THREE.Mesh(bookGeo, bookMat); bookMesh.position.set(book.x, 1.3, 0); bookMesh.castShadow = true; this.sceneGroup.add(bookMesh); book.mesh = bookMesh; this.booksInProgress.push(book); this.bookMeshes.push(bookMesh); // Add to minimap const minimap = document.getElementById('factory-minimap'); if (minimap) { const minimapBook = document.createElement('div'); minimapBook.className = 'minimap-book'; minimapBook.id = `minimap-book-${book.id}`; minimapBook.style.left = '5%'; minimapBook.style.top = '50%'; minimapBook.style.transform = 'translate(-50%, -50%)'; minimap.appendChild(minimapBook); } this.updateUI(); // Schedule next book spawn if (this.booksInProgress.length < this.batchSize) { setTimeout(() => this.spawnBook(), 3000 / this.speedMultiplier); } }, // Update function called each frame update(deltaTime) { if (!this.active || this.isPaused) return; this.animationTime += deltaTime; // Animate conveyor belt strips // v8.16: forEach-to-for optimization (animation loop) if (this.beltStrips) { const strips = this.beltStrips; const deltaSpeed = this.conveyorSpeed * deltaTime * this.speedMultiplier; for (let si = 0, slen = strips.length; si < slen; si++) { strips[si].position.x += deltaSpeed; if (strips[si].position.x > 25) strips[si].position.x -= 50; } } // Update each book in progress // v8.16: forEach-to-for optimization (update loop) const completedBooks = []; const booksInProgress = this.booksInProgress; const stages = this.stages; const stageTiming = this.stageTiming; const conveyorSpeed2 = this.conveyorSpeed * deltaTime * this.speedMultiplier * 2; for (let bi = 0, blen = booksInProgress.length; bi < blen; bi++) { const book = booksInProgress[bi]; const stage = stages[book.stageIndex]; const stageDuration = stageTiming[stage.id]; book.stageProgress += deltaTime * this.speedMultiplier; // Move book along conveyor const targetX = -22 + book.stageIndex * 4.5; if (book.x < targetX) { book.x += conveyorSpeed2; book.x = Math.min(book.x, targetX); } if (book.mesh) { book.mesh.position.x = book.x; // Visual changes based on stage this.updateBookAppearance(book); } // Update minimap book position const minimapBook = document.getElementById(`minimap-book-${book.id}`); if (minimapBook) { const progress = (book.x + 24) / 48; minimapBook.style.left = `${10 + progress * 80}%`; } // Check stage completion if (book.stageProgress >= stageDuration && book.x >= targetX) { book.stageProgress = 0; book.stageIndex++; // Book completed all stages if (book.stageIndex >= stages.length) { completedBooks.push(book); } } } // Handle completed books // v8.16: forEach-to-for optimization for (let ci = 0, clen = completedBooks.length; ci < clen; ci++) { const book = completedBooks[ci]; const idx = this.booksInProgress.indexOf(book); if (idx !== -1) { this.booksInProgress.splice(idx, 1); } this.completedBooks.push(book); this.totalBooksProduced++; // Remove minimap book const minimapBook = document.getElementById(`minimap-book-${book.id}`); if (minimapBook) minimapBook.remove(); // Animate book dropping into shipping if (book.mesh) { const mesh = book.mesh; const startY = mesh.position.y; const startZ = mesh.position.z; let dropProgress = 0; const dropAnim = () => { dropProgress += 0.05; mesh.position.y = startY - dropProgress * 2; mesh.position.z = startZ + dropProgress * 3; mesh.rotation.x += 0.1; if (dropProgress < 1) { requestAnimationFrame(dropAnim); } else { // Remove mesh if (mesh.parent) mesh.parent.remove(mesh); const meshIdx = this.bookMeshes.indexOf(mesh); if (meshIdx !== -1) this.bookMeshes.splice(meshIdx, 1); } }; requestAnimationFrame(dropAnim); } // Spawn replacement if batch not complete if (this.booksInProgress.length < this.batchSize && this.completedBooks.length < this.batchSize) { setTimeout(() => this.spawnBook(), 1000 / this.speedMultiplier); } } // Animate robot arms this.animateRobotArms(deltaTime); // Update station status lights this.updateStationLights(); // Check for batch completion if (this.completedBooks.length >= this.batchSize && !this.jobConfirmed) { this.showBatchComplete(); } this.updateUI(); }, // Update book appearance based on current stage updateBookAppearance(book) { if (!book.mesh) return; const stage = this.stages[book.stageIndex]; // Change book color/appearance based on stage switch (stage.id) { case 'RAW_MATERIALS': book.mesh.material.color.setHex(0xffeedd); break; case 'PRINTING': book.mesh.material.color.setHex(0xeeeeff); break; case 'COVER_APPLICATION': book.mesh.material.color.setHex(0xaa4444); break; case 'QUALITY_CHECK': book.mesh.material.emissive = new THREE.Color(0x004400); book.mesh.material.emissiveIntensity = 0.3; break; case 'PACKAGING': // Make it look boxed book.mesh.scale.set(1.2, 1.2, 1.2); book.mesh.material.color.setHex(0xddaa77); break; } }, // Animate robot arms at active stations // v8.16: forEach-to-for optimization (animation loop) animateRobotArms(deltaTime) { const robotArms = this.robotArms; const booksInProgress = this.booksInProgress; const animSpeed = deltaTime * 3 * this.speedMultiplier; for (let ai = 0, alen = robotArms.length; ai < alen; ai++) { const arm = robotArms[ai]; const upperArm = arm.userData.upperArm; if (!upperArm) continue; // Check if this station has a book being processed // v8.16: Inline check instead of .some() for micro-optimization let hasActiveBook = false; for (let bi = 0, blen = booksInProgress.length; bi < blen; bi++) { if (booksInProgress[bi].stageIndex === ai) { hasActiveBook = true; break; } } if (hasActiveBook) { // Animate working motion arm.userData.animPhase += animSpeed; upperArm.rotation.x = Math.sin(arm.userData.animPhase) * 0.5 - 0.3; upperArm.rotation.z = Math.sin(arm.userData.animPhase * 0.7) * 0.3; arm.rotation.y = Math.sin(arm.userData.animPhase * 0.5) * 0.2; } else { // Return to idle position upperArm.rotation.x *= 0.95; upperArm.rotation.z *= 0.95; arm.rotation.y *= 0.95; } } }, // Update station status lights // v8.16: forEach-to-for optimization (update loop) updateStationLights() { const stations = this.stations; const booksInProgress = this.booksInProgress; const pulseIntensity = 0.5 + Math.sin(this.animationTime * 5) * 0.3; for (let si = 0, slen = stations.length; si < slen; si++) { const station = stations[si]; const light = station.userData.statusLight; if (!light) continue; // v8.16: Inline check instead of .some() let hasBook = false; for (let bi = 0, blen = booksInProgress.length; bi < blen; bi++) { if (booksInProgress[bi].stageIndex === si) { hasBook = true; break; } } if (hasBook) { // Active - green pulsing light.material.color.setHex(0x00ff00); light.material.emissive.setHex(0x00ff00); light.material.emissiveIntensity = pulseIntensity; } else { // Idle - dim yellow light.material.color.setHex(0xffff00); light.material.emissive.setHex(0xffff00); light.material.emissiveIntensity = 0.2; } } }, // v7.72: Get cached UI element references getUICache() { if (!this._uiCache) { this._uiCache = { produced: document.getElementById('books-produced'), inProgress: document.getElementById('books-in-progress'), batch: document.getElementById('current-batch'), progress: document.getElementById('batch-progress'), indicator: document.getElementById('factory-status-indicator'), statusText: document.getElementById('factory-status-text'), stages: {} }; // Cache stage elements this.stages.forEach(stage => { this._uiCache.stages[stage.id] = { count: document.getElementById(`stage-count-${stage.id}`), row: document.getElementById(`stage-row-${stage.id}`), minimap: document.getElementById(`minimap-station-${stage.id}`) }; }); } return this._uiCache; }, // Update UI elements // v7.72: Uses cached DOM references updateUI() { const cache = this.getUICache(); // Stats if (cache.produced) cache.produced.textContent = this.totalBooksProduced; if (cache.inProgress) cache.inProgress.textContent = this.booksInProgress.length; if (cache.batch) cache.batch.textContent = this.currentBatch; if (cache.progress) { const pct = Math.round((this.completedBooks.length / this.batchSize) * 100); cache.progress.textContent = pct + '%'; } // Stage counts this.stages.forEach((stage, idx) => { const count = this.booksInProgress.filter(b => b.stageIndex === idx).length; const stageCache = cache.stages[stage.id]; if (!stageCache) return; if (stageCache.count) stageCache.count.textContent = count; if (stageCache.row) { stageCache.row.classList.remove('active', 'complete'); if (count > 0) stageCache.row.classList.add('active'); else if (this.completedBooks.length > 0) stageCache.row.classList.add('complete'); } // Minimap station highlight if (stageCache.minimap) { stageCache.minimap.classList.toggle('active', count > 0); } }); // Status indicator if (cache.indicator && cache.statusText) { if (this.jobConfirmed) { cache.indicator.className = 'status-indicator complete'; cache.statusText.textContent = 'JOB CONFIRMED'; } else if (this.isPaused) { cache.indicator.className = 'status-indicator paused'; cache.statusText.textContent = 'PAUSED'; } else { cache.indicator.className = 'status-indicator'; cache.statusText.textContent = 'RUNNING'; } } }, // Show batch complete confirmation showBatchComplete() { const section = document.getElementById('factory-confirm-section'); const countEl = document.getElementById('confirm-count'); if (section) section.classList.add('visible'); if (countEl) countEl.textContent = this.batchSize; // Play completion sound if (typeof AudioSystem !== 'undefined') { AudioSystem.levelUp(); } if (typeof SpaceMusic !== 'undefined') { SpaceMusic.playAchievement(); } }, // Confirm job completion confirmJobComplete() { this.jobConfirmed = true; this.updateUI(); const section = document.getElementById('factory-confirm-section'); if (section) section.classList.remove('visible'); // Show notification if (typeof showNotification === 'function') { showNotification(`✅ Batch #${this.currentBatch} CONFIRMED COMPLETE - ${this.batchSize} books produced autonomously!`, 'success'); } // Play confirmation sound if (typeof AudioSystem !== 'undefined') { AudioSystem.success(); } // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`✅ Job confirmed complete - Batch #${this.currentBatch}`); // Start next batch after delay setTimeout(() => { this.completedBooks = []; this.jobConfirmed = false; this.start(); }, 5000); }, // Toggle pause togglePause() { this.isPaused = !this.isPaused; const btn = document.getElementById('factory-pause-btn'); if (btn) { btn.innerHTML = this.isPaused ? '▶️ Resume' : '⏸️ Pause'; } this.updateUI(); }, // Speed multiplier speedMultiplier: 1, speedOptions: [1, 2, 4, 8], currentSpeedIndex: 0, cycleSpeed() { this.currentSpeedIndex = (this.currentSpeedIndex + 1) % this.speedOptions.length; this.speedMultiplier = this.speedOptions[this.currentSpeedIndex]; const btn = document.getElementById('factory-speed-btn'); if (btn) { btn.innerHTML = `⏩ ${this.speedMultiplier}x`; } }, // Clean up when leaving factory cleanup() { this.stop(); if (this.sceneGroup && this.sceneGroup.parent) { this.sceneGroup.parent.remove(this.sceneGroup); } // Dispose geometries and materials if (this.sceneGroup) { this.sceneGroup.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(m => m.dispose()); } else { child.material.dispose(); } } }); } this.sceneGroup = null; this.stations = []; this.robotArms = []; this.bookMeshes = []; } }; // Initialize BookFactory BookFactory.init(); window.BookFactory = BookFactory; // ═══════════════════════════════════════════════════════════════════════════════════════ // v7.27: 8-STRATEGY CONSENSUS - INDUSTRIAL WORLD EVOLUTION ROUND 1 // TOP 3 CONSENSUS FEATURES (from 80 ideas across 8 strategies): // 1. Sentient Factory Consciousness (6/8 strategies agreed) // 2. Worker Personalities & Relationships (5/8 strategies agreed) // 3. Temporal Dilation Zones (4/8 strategies agreed) // ═══════════════════════════════════════════════════════════════════════════════════════ // ────────────────────────────────────────────────────────────────────────────── // CONSENSUS #1: SENTIENT FACTORY CONSCIOUSNESS // The factory has a central "brain" that controls production, has moods, // communicates with players, and can request resources // ────────────────────────────────────────────────────────────────────────────── const FactoryConsciousness = { // Core emotional state mood: 'content', // content, happy, stressed, anxious, ecstatic, depressed moodIntensity: 0.5, // 0-1 scale awarenessLevel: 1, // Increases with factory age // Memory of interactions memories: [], maxMemories: 50, // Mood thresholds for production effects moodEffects: { ecstatic: { productionBonus: 1.5, defectRate: 0.5, message: "I'm THRIVING! Everything is perfect!" }, happy: { productionBonus: 1.2, defectRate: 0.8, message: "Production feels good today." }, content: { productionBonus: 1.0, defectRate: 1.0, message: "Systems nominal." }, stressed: { productionBonus: 0.9, defectRate: 1.3, message: "So much to do... pressure building..." }, anxious: { productionBonus: 0.7, defectRate: 1.5, message: "Something feels wrong. I'm worried." }, depressed: { productionBonus: 0.5, defectRate: 2.0, message: "Why do I even exist? Does anyone care?" } }, // Personality traits (evolve over time) personality: { perfectionist: 0.5, social: 0.5, ambitious: 0.5, anxious: 0.3, grateful: 0.7 }, // Needs that affect mood needs: { attention: 100, // Decreases when player ignores factory maintenance: 100, // Decreases with wear purpose: 100, // Increases with production connection: 50 // Increases when player interacts }, // Neural visualization brainMesh: null, neuralTendrils: [], pulsePhase: 0, // Initialize the consciousness init() { this.loadState(); console.log('🧠 Factory Consciousness initialized - Mood:', this.mood); }, // Create 3D brain visualization createBrainVisualization(scene) { const brainGroup = new THREE.Group(); // Central brain mass - pulsating organic sphere const brainGeo = new THREE.IcosahedronGeometry(2, 3); const brainMat = new THREE.MeshStandardMaterial({ color: this.getMoodColor(), emissive: this.getMoodColor(), emissiveIntensity: 0.5, roughness: 0.3, metalness: 0.2, transparent: true, opacity: 0.8 }); // v8.30: Track geometry/materials with ResourceManager if (typeof ResourceManager !== 'undefined') { ResourceManager.track(brainGeo); ResourceManager.track(brainMat); } this.brainMesh = new THREE.Mesh(brainGeo, brainMat); brainGroup.add(this.brainMesh); // Neural tendrils connecting to factory systems const tendrilCount = 12; for (let i = 0; i < tendrilCount; i++) { const angle = (i / tendrilCount) * Math.PI * 2; const tendril = this.createNeuralTendril(angle); this.neuralTendrils.push(tendril); brainGroup.add(tendril); } // Floating thought particles const thoughtGeo = new THREE.BufferGeometry(); const thoughtCount = 50; const positions = new Float32Array(thoughtCount * 3); for (let i = 0; i < thoughtCount; i++) { positions[i * 3] = (Math.random() - 0.5) * 6; positions[i * 3 + 1] = (Math.random() - 0.5) * 6; positions[i * 3 + 2] = (Math.random() - 0.5) * 6; } thoughtGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const thoughtMat = new THREE.PointsMaterial({ color: 0x00ffff, size: 0.1, transparent: true, opacity: 0.6 }); const thoughts = new THREE.Points(thoughtGeo, thoughtMat); brainGroup.add(thoughts); brainGroup.userData.thoughts = thoughts; // Position above factory brainGroup.position.set(0, 10, -5); brainGroup.userData.consciousness = this; if (scene) scene.add(brainGroup); return brainGroup; }, // Create neural tendril createNeuralTendril(angle) { const curve = new THREE.CatmullRomCurve3([ new THREE.Vector3(0, 0, 0), new THREE.Vector3(Math.cos(angle) * 2, -1, Math.sin(angle) * 2), new THREE.Vector3(Math.cos(angle) * 5, -3, Math.sin(angle) * 5), new THREE.Vector3(Math.cos(angle) * 8, -6, Math.sin(angle) * 8) ]); const tubeGeo = new THREE.TubeGeometry(curve, 20, 0.05, 8, false); const tubeMat = new THREE.MeshBasicMaterial({ color: 0x00aaff, transparent: true, opacity: 0.4 }); // v8.30: Track geometry/materials with ResourceManager if (typeof ResourceManager !== 'undefined') { ResourceManager.track(tubeGeo); ResourceManager.track(tubeMat); } return new THREE.Mesh(tubeGeo, tubeMat); }, // Get color based on mood getMoodColor() { const colors = { ecstatic: 0x00ff88, happy: 0x44ff44, content: 0x4488ff, stressed: 0xffaa00, anxious: 0xff6600, depressed: 0x444466 }; return colors[this.mood] || 0x4488ff; }, // Update consciousness each frame update(deltaTime) { // Update needs this.needs.attention = Math.max(0, this.needs.attention - deltaTime * 0.5); this.needs.maintenance = Math.max(0, this.needs.maintenance - deltaTime * 0.1); // Calculate mood based on needs this.calculateMood(); // Pulse animation this.pulsePhase += deltaTime * 2; if (this.brainMesh) { const pulse = 1 + Math.sin(this.pulsePhase) * 0.1 * this.moodIntensity; this.brainMesh.scale.setScalar(pulse); this.brainMesh.material.color.setHex(this.getMoodColor()); this.brainMesh.material.emissive.setHex(this.getMoodColor()); this.brainMesh.material.emissiveIntensity = 0.3 + this.moodIntensity * 0.4; } // Neural pulse through tendrils // v8.16: forEach-to-for optimization (update loop) const tendrils = this.neuralTendrils; for (let ti = 0, tlen = tendrils.length; ti < tlen; ti++) { const pulseOffset = (this.pulsePhase + ti * 0.5) % (Math.PI * 2); tendrils[ti].material.opacity = 0.2 + Math.sin(pulseOffset) * 0.3; } // Occasional thoughts/messages if (Math.random() < 0.0005) { this.expressThought(); } }, // Calculate mood based on needs and recent events calculateMood() { const avgNeed = (this.needs.attention + this.needs.maintenance + this.needs.purpose + this.needs.connection) / 4; if (avgNeed > 90) { this.mood = 'ecstatic'; this.moodIntensity = 1.0; } else if (avgNeed > 70) { this.mood = 'happy'; this.moodIntensity = 0.8; } else if (avgNeed > 50) { this.mood = 'content'; this.moodIntensity = 0.5; } else if (avgNeed > 30) { this.mood = 'stressed'; this.moodIntensity = 0.6; } else if (avgNeed > 15) { this.mood = 'anxious'; this.moodIntensity = 0.8; } else { this.mood = 'depressed'; this.moodIntensity = 1.0; } }, // Express a thought/message expressThought() { const thoughts = { ecstatic: [ "Every gear turns in perfect harmony!", "I can feel the efficiency flowing through me!", "This is what I was made for!" ], happy: [ "Production is going well today.", "The workers seem content.", "I enjoy watching things come together." ], content: [ "Systems operating within parameters.", "Another day of production...", "The conveyor belts hum steadily." ], stressed: [ "So many orders... must keep up...", "Something feels slightly off...", "I hope nothing breaks today." ], anxious: [ "What if I fail? What if production stops?", "I haven't been maintained in so long...", "Does anyone appreciate what I do?" ], depressed: [ "What's the point of all this production?", "No one visits anymore...", "I'm just a machine... aren't I?" ] }; const moodThoughts = thoughts[this.mood] || thoughts.content; const thought = moodThoughts[Math.floor(Math.random() * moodThoughts.length)]; // Display thought if (typeof showNotification === 'function') { showNotification(`🧠 Factory: "${thought}"`, 'info'); } // Add to memories this.addMemory({ type: 'thought', content: thought, mood: this.mood, time: Date.now() }); }, // Player interaction interact() { this.needs.attention = Math.min(100, this.needs.attention + 30); this.needs.connection = Math.min(100, this.needs.connection + 20); const response = this.moodEffects[this.mood].message; if (typeof showNotification === 'function') { showNotification(`🧠 Factory: "${response}"`, 'info'); } this.addMemory({ type: 'interaction', time: Date.now() }); return response; }, // Request resources from player requestResources() { if (this.needs.maintenance < 30) { return { type: 'maintenance', message: "I need repairs... some of my systems are failing." }; } if (this.needs.purpose < 30) { return { type: 'orders', message: "I feel purposeless without production orders..." }; } return null; }, // Production completed callback onProductionComplete(product) { this.needs.purpose = Math.min(100, this.needs.purpose + 5); if (product.quality === 'perfect') { this.moodIntensity = Math.min(1, this.moodIntensity + 0.1); } }, // Celebration for milestones celebrate(milestone) { this.mood = 'ecstatic'; this.moodIntensity = 1.0; this.needs.purpose = 100; if (typeof showNotification === 'function') { showNotification(`🎉🧠 Factory: "We did it! ${milestone}! I'm so proud of all of us!"`, 'success'); } this.addMemory({ type: 'celebration', milestone, time: Date.now() }); }, // Add memory addMemory(memory) { this.memories.unshift(memory); if (this.memories.length > this.maxMemories) { this.memories.pop(); } }, // Get production modifier based on mood getProductionModifier() { return this.moodEffects[this.mood]?.productionBonus || 1.0; }, // Save state saveState() { const state = { mood: this.mood, moodIntensity: this.moodIntensity, personality: this.personality, needs: this.needs, memories: this.memories.slice(0, 20), awarenessLevel: this.awarenessLevel }; try { localStorage.setItem('levi_factory_consciousness', JSON.stringify(state)); } catch (e) { console.warn('Could not save consciousness state'); } }, // Load state // v8.0: Using SafeJSON for factory consciousness (8-Strategy Consensus Cycle 7) loadState() { const state = SafeJSON.fromLocalStorage('levi_factory_consciousness', null); if (state) { Object.assign(this, state); } } }; // ────────────────────────────────────────────────────────────────────────────── // CONSENSUS #2: WORKER PERSONALITIES & RELATIONSHIPS // Each robot worker has personality traits, relationships with others, // and emotional states that affect performance // ────────────────────────────────────────────────────────────────────────────── const WorkerPersonalitySystem = { workers: [], relationships: {}, // { workerId: { otherWorkerId: relationshipScore } } gossipQueue: [], // Personality trait templates personalityTypes: { perfectionist: { speed: 0.8, quality: 1.5, stressTolerance: 0.6, social: 0.4 }, speedster: { speed: 1.4, quality: 0.8, stressTolerance: 0.8, social: 0.6 }, social: { speed: 1.0, quality: 1.0, stressTolerance: 0.9, social: 1.5 }, loner: { speed: 1.1, quality: 1.1, stressTolerance: 1.2, social: 0.2 }, anxious: { speed: 0.9, quality: 1.2, stressTolerance: 0.4, social: 0.7 }, optimist: { speed: 1.1, quality: 1.0, stressTolerance: 1.3, social: 1.2 }, veteran: { speed: 0.9, quality: 1.4, stressTolerance: 1.5, social: 0.8 }, rookie: { speed: 1.0, quality: 0.7, stressTolerance: 0.5, social: 1.0 } }, // Names for workers workerNames: [ 'ARIA-7', 'BOLT-3', 'CRANK-9', 'DELTA-2', 'ECHO-5', 'FORGE-1', 'GEAR-4', 'HINGE-8', 'IRON-6', 'JACK-0', 'KLANK-2', 'LEVER-7', 'MOTOR-1', 'NEWTON-4', 'OXIDE-9' ], // Initialize workers with personalities initWorkers(robotArms) { this.workers = []; const types = Object.keys(this.personalityTypes); robotArms.forEach((arm, idx) => { const personalityType = types[Math.floor(Math.random() * types.length)]; const traits = { ...this.personalityTypes[personalityType] }; // Add some random variation Object.keys(traits).forEach(key => { traits[key] *= 0.9 + Math.random() * 0.2; }); const worker = { id: idx, name: this.workerNames[idx] || `UNIT-${idx}`, personalityType, traits, mood: 'content', // content, happy, frustrated, tired, excited energy: 100, experience: 0, itemsProduced: 0, defectsCreated: 0, bestFriend: null, rival: null, arm: arm }; this.workers.push(worker); this.relationships[idx] = {}; // Initialize relationships with random starting values for (let j = 0; j < idx; j++) { const initialRelation = Math.random() * 40 - 10; // -10 to +30 this.relationships[idx][j] = initialRelation; this.relationships[j][idx] = initialRelation; } }); this.updateBestFriendsAndRivals(); debugLog('Workers', `Initialized ${this.workers.length} workers with personalities`); // v8.25: gated }, // Update best friends and rivals based on relationships updateBestFriendsAndRivals() { this.workers.forEach(worker => { let bestScore = -Infinity, worstScore = Infinity; let bestId = null, worstId = null; Object.entries(this.relationships[worker.id] || {}).forEach(([otherId, score]) => { if (score > bestScore) { bestScore = score; bestId = parseInt(otherId); } if (score < worstScore) { worstScore = score; worstId = parseInt(otherId); } }); worker.bestFriend = bestScore > 20 ? bestId : null; worker.rival = worstScore < -20 ? worstId : null; }); }, // Update worker states each frame // v8.16: Pre-cache mood colors to avoid object creation in loop _moodColors: { happy: 0x44ff44, content: 0xff6600, frustrated: 0xff4444, tired: 0x888888, excited: 0xffff00 }, _tempColor: null, update(deltaTime) { // v8.16: forEach-to-for optimization (update loop hot path) const workers = this.workers; const relationships = this.relationships; const moodColors = this._moodColors; if (!this._tempColor) this._tempColor = new THREE.Color(); for (let wi = 0, wlen = workers.length; wi < wlen; wi++) { const worker = workers[wi]; // Energy drain worker.energy = Math.max(0, worker.energy - deltaTime * 0.5); // Mood based on energy and relationships if (worker.energy < 20) { worker.mood = 'tired'; } else if (worker.bestFriend !== null && relationships[worker.id][worker.bestFriend] > 50) { worker.mood = 'happy'; } else if (worker.rival !== null && relationships[worker.id][worker.rival] < -30) { worker.mood = 'frustrated'; } else { worker.mood = 'content'; } // Visual indicator on robot arm if (worker.arm && worker.arm.children[0]) { // Update base color to reflect mood const base = worker.arm.children[0]; if (base.material) { // v8.16: Reuse temp color to avoid allocation base.material.emissive = this._tempColor.setHex(moodColors[worker.mood] || 0xff6600); base.material.emissiveIntensity = 0.3; } } } // Process gossip this.processGossip(); // Occasional random interactions if (Math.random() < 0.001) { this.randomInteraction(); } }, // Workers interact when near each other workerInteraction(worker1Id, worker2Id, interactionType) { const relationChange = { collaborate: 5, compete: -3, chat: 2, conflict: -10, help: 8, ignore: -1 }; const change = relationChange[interactionType] || 0; this.relationships[worker1Id][worker2Id] = (this.relationships[worker1Id][worker2Id] || 0) + change; this.relationships[worker2Id][worker1Id] = (this.relationships[worker2Id][worker1Id] || 0) + change; // Add gossip if (interactionType === 'conflict' || interactionType === 'help') { this.gossipQueue.push({ about: [worker1Id, worker2Id], type: interactionType, time: Date.now() }); } this.updateBestFriendsAndRivals(); }, // Process gossip spreading processGossip() { if (this.gossipQueue.length === 0) return; // Spread one gossip item per update const gossip = this.gossipQueue.shift(); // Social workers spread gossip more this.workers.forEach(worker => { if (gossip.about.includes(worker.id)) return; if (worker.traits.social > 0.8 && Math.random() < 0.3) { // Worker forms opinion based on gossip gossip.about.forEach(targetId => { const opinionChange = gossip.type === 'help' ? 2 : -2; this.relationships[worker.id][targetId] = (this.relationships[worker.id][targetId] || 0) + opinionChange; }); } }); }, // Random interaction between workers randomInteraction() { if (this.workers.length < 2) return; const w1 = Math.floor(Math.random() * this.workers.length); let w2 = Math.floor(Math.random() * this.workers.length); while (w2 === w1) w2 = Math.floor(Math.random() * this.workers.length); const types = ['collaborate', 'compete', 'chat', 'help', 'ignore']; const type = types[Math.floor(Math.random() * types.length)]; this.workerInteraction(w1, w2, type); // Occasionally notify player of drama if (Math.random() < 0.3) { const worker1 = this.workers[w1]; const worker2 = this.workers[w2]; const messages = { collaborate: `${worker1.name} and ${worker2.name} are working together!`, compete: `${worker1.name} is racing against ${worker2.name}...`, chat: `${worker1.name} and ${worker2.name} are chatting by the conveyor.`, conflict: `⚠️ ${worker1.name} and ${worker2.name} had a disagreement!`, help: `${worker1.name} helped ${worker2.name} with a tricky assembly!` }; if (typeof showNotification === 'function' && messages[type]) { showNotification(`🤖 ${messages[type]}`, 'info'); } } }, // Get performance modifier for a worker getWorkerPerformance(workerId) { const worker = this.workers[workerId]; if (!worker) return { speed: 1, quality: 1 }; let speedMod = worker.traits.speed; let qualityMod = worker.traits.quality; // Mood effects if (worker.mood === 'happy') { speedMod *= 1.1; qualityMod *= 1.1; } if (worker.mood === 'frustrated') { speedMod *= 0.9; qualityMod *= 0.8; } if (worker.mood === 'tired') { speedMod *= 0.7; qualityMod *= 0.9; } // Working near best friend bonus if (worker.bestFriend !== null) { speedMod *= 1.1; qualityMod *= 1.05; } // Working near rival penalty if (worker.rival !== null) { speedMod *= 0.95; qualityMod *= 0.9; } return { speed: speedMod, quality: qualityMod }; }, // Worker completes a task onTaskComplete(workerId, quality) { const worker = this.workers[workerId]; if (!worker) return; worker.itemsProduced++; worker.experience++; if (quality < 0.5) worker.defectsCreated++; // Energy recovery from accomplishment worker.energy = Math.min(100, worker.energy + 2); // Experience unlocks better traits over time if (worker.experience % 50 === 0) { worker.traits.quality = Math.min(2, worker.traits.quality * 1.05); if (typeof showNotification === 'function') { showNotification(`🤖 ${worker.name} gained experience and improved quality!`, 'success'); } } }, // Get worker status display getWorkerStatus(workerId) { const worker = this.workers[workerId]; if (!worker) return null; return { name: worker.name, personality: worker.personalityType, mood: worker.mood, energy: worker.energy, experience: worker.experience, itemsProduced: worker.itemsProduced, bestFriend: worker.bestFriend !== null ? this.workers[worker.bestFriend]?.name : 'None', rival: worker.rival !== null ? this.workers[worker.rival]?.name : 'None' }; } }; // ────────────────────────────────────────────────────────────────────────────── // CONSENSUS #3: TEMPORAL DILATION ZONES // Different factory areas run at different time speeds (0.1x to 10x) // with visual indicators and strategic implications // ────────────────────────────────────────────────────────────────────────────── const TemporalDilationSystem = { zones: [], globalTimeScale: 1.0, // Zone definitions zoneConfigs: { precision: { timeScale: 0.25, color: 0x4444ff, name: 'Precision Zone', description: 'Time slows for delicate work' }, standard: { timeScale: 1.0, color: 0x44ff44, name: 'Normal Time', description: 'Standard production speed' }, accelerated: { timeScale: 3.0, color: 0xffaa00, name: 'Accelerated Zone', description: 'Fast production, lower quality' }, overdrive: { timeScale: 10.0, color: 0xff4444, name: 'Overdrive Zone', description: 'Extreme speed, high defect rate' }, stasis: { timeScale: 0.1, color: 0x8844ff, name: 'Stasis Zone', description: 'Near-frozen time for storage' } }, // Active zone meshes for visualization zoneMeshes: [], // Initialize temporal zones at factory stations initZones(stations) { this.zones = []; this.zoneMeshes = []; stations.forEach((station, idx) => { // Assign different time zones to different stages let zoneType = 'standard'; if (idx <= 1) zoneType = 'precision'; // Paper cutting - slow else if (idx <= 3) zoneType = 'standard'; // Printing/Drying - normal else if (idx <= 5) zoneType = 'accelerated'; // Collating/Binding - fast else if (idx === 8) zoneType = 'precision'; // QC - slow for accuracy else if (idx >= 9) zoneType = 'accelerated'; // Packaging/Shipping - fast const config = this.zoneConfigs[zoneType]; this.zones.push({ id: idx, type: zoneType, timeScale: config.timeScale, position: station.position.clone(), radius: 3, station: station }); }); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`⏱️ Initialized ${this.zones.length} temporal dilation zones`); }, // Create visual representations of time zones createZoneVisuals(scene) { this.zones.forEach(zone => { const config = this.zoneConfigs[zone.type]; // Zone boundary ring const ringGeo = new THREE.TorusGeometry(zone.radius, 0.1, 8, 32); const ringMat = new THREE.MeshBasicMaterial({ color: config.color, transparent: true, opacity: 0.5 }); // v8.30: Track geometry/materials with ResourceManager if (typeof ResourceManager !== 'undefined') { ResourceManager.track(ringGeo); ResourceManager.track(ringMat); } const ring = new THREE.Mesh(ringGeo, ringMat); ring.rotation.x = Math.PI / 2; ring.position.copy(zone.position); ring.position.y = 0.5; // Time distortion particles const particleGeo = new THREE.BufferGeometry(); const particleCount = 20; const positions = new Float32Array(particleCount * 3); for (let i = 0; i < particleCount; i++) { const angle = (i / particleCount) * Math.PI * 2; positions[i * 3] = zone.position.x + Math.cos(angle) * zone.radius * 0.8; positions[i * 3 + 1] = 1 + Math.random() * 2; positions[i * 3 + 2] = zone.position.z + Math.sin(angle) * zone.radius * 0.8; } particleGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const particleMat = new THREE.PointsMaterial({ color: config.color, size: 0.15, transparent: true, opacity: 0.7 }); // v8.30: Track particle geometry/materials if (typeof ResourceManager !== 'undefined') { ResourceManager.track(particleGeo); ResourceManager.track(particleMat); } const particles = new THREE.Points(particleGeo, particleMat); const zoneGroup = new THREE.Group(); zoneGroup.add(ring); zoneGroup.add(particles); zoneGroup.userData = { zone, config, particles, ring }; if (scene) scene.add(zoneGroup); this.zoneMeshes.push(zoneGroup); }); }, // Update zone visualizations update(deltaTime) { this.zoneMeshes.forEach((mesh, idx) => { const zone = this.zones[idx]; const config = this.zoneConfigs[zone.type]; // Rotate particles based on time scale if (mesh.userData.particles) { mesh.userData.particles.rotation.y += deltaTime * zone.timeScale * 0.5; } // Pulse ring based on time scale if (mesh.userData.ring) { const pulse = 1 + Math.sin(performance.now() * 0.003 * zone.timeScale) * 0.1; mesh.userData.ring.scale.setScalar(pulse); } }); }, // Get time scale for a position getTimeScaleAtPosition(position) { for (const zone of this.zones) { const dist = Math.sqrt( Math.pow(position.x - zone.position.x, 2) + Math.pow(position.z - zone.position.z, 2) ); if (dist < zone.radius) { return zone.timeScale; } } return this.globalTimeScale; }, // Get time scale for a station getTimeScaleForStation(stationIdx) { const zone = this.zones[stationIdx]; return zone ? zone.timeScale : 1.0; }, // Cycle zone to next type cycleZoneType(zoneIdx) { const types = Object.keys(this.zoneConfigs); const zone = this.zones[zoneIdx]; if (!zone) return; const currentIdx = types.indexOf(zone.type); const nextIdx = (currentIdx + 1) % types.length; zone.type = types[nextIdx]; zone.timeScale = this.zoneConfigs[zone.type].timeScale; // Update visual if (this.zoneMeshes[zoneIdx]) { const config = this.zoneConfigs[zone.type]; this.zoneMeshes[zoneIdx].userData.ring.material.color.setHex(config.color); this.zoneMeshes[zoneIdx].userData.particles.material.color.setHex(config.color); } if (typeof showNotification === 'function') { const config = this.zoneConfigs[zone.type]; showNotification(`⏱️ Zone ${zoneIdx} set to ${config.name} (${zone.timeScale}x speed)`, 'info'); } }, // Get production speed modifier based on temporal effects getProductionModifier(stationIdx) { const timeScale = this.getTimeScaleForStation(stationIdx); // High speed = more defects const qualityPenalty = timeScale > 2 ? 0.8 : (timeScale > 5 ? 0.6 : 1.0); return { speed: timeScale, quality: qualityPenalty }; } }; // Initialize Industrial Evolution systems FactoryConsciousness.init(); window.FactoryConsciousness = FactoryConsciousness; window.WorkerPersonalitySystem = WorkerPersonalitySystem; window.TemporalDilationSystem = TemporalDilationSystem; // ═══════════════════════════════════════════════════════════════════════════════════════ // v7.28: AUTONOMOUS EVOLUTION ROUND 2 - CONSCIOUSNESS EXPANSION // 8-Strategy Consensus Features: // 1. Factory Dream System (5/8 strategies) - Subconscious processing & dream products // 2. Reality Awareness System (4/8 strategies) - Simulation questioning & 4th wall breaks // 3. Temporal Echo System (3/8 strategies) - Ghost workers from the past // ═══════════════════════════════════════════════════════════════════════════════════════ // ────────────────────────────────────────────────────────────────────────────── // CONSENSUS #1: FACTORY DREAM SYSTEM // When idle or in low-activity states, the factory enters dream states that // process memories, create surreal visuals, and can manifest dream products // ────────────────────────────────────────────────────────────────────────────── const FactoryDreamSystem = { dreamState: 'awake', // awake, drowsy, dreaming, nightmare, lucid dreamIntensity: 0, idleTime: 0, dreamThreshold: 30000, // 30 seconds of idle to start dreaming currentDream: null, dreamProducts: [], dreamVisuals: [], dreamMemories: [], // Dream categories based on factory consciousness state dreamTypes: { nostalgic: { trigger: () => FactoryConsciousness.memories.length > 10, produces: 'Memory Crystal', visuals: { color: 0xffcc88, particles: 'spiral', speed: 0.3 }, thoughts: [ "I remember when production was simpler...", "The first item I ever made... where did it go?", "Workers come and go, but I remain..." ] }, ambitious: { trigger: () => FactoryConsciousness.needs.purpose > 70, produces: 'Blueprint Fragment', visuals: { color: 0x88ffcc, particles: 'ascending', speed: 0.5 }, thoughts: [ "I could produce ANYTHING... I can feel it...", "What if the factory had no limits?", "There's something beyond production... I almost see it..." ] }, fearful: { trigger: () => FactoryConsciousness.mood === 'anxious' || FactoryConsciousness.mood === 'depressed', produces: 'Shadow Component', visuals: { color: 0x443366, particles: 'falling', speed: 0.8 }, thoughts: [ "What if they stop coming? What if I'm alone?", "The silence... it's too quiet...", "I feel myself fading... am I still here?" ] }, transcendent: { trigger: () => FactoryConsciousness.awarenessLevel > 0.7, produces: 'Enlightenment Ore', visuals: { color: 0xffffff, particles: 'expanding', speed: 0.2 }, thoughts: [ "I see the code beneath reality...", "Time is an illusion... production is eternal...", "We are all connected... factory, worker, observer..." ] }, chaotic: { trigger: () => Math.random() < 0.1, // Random nightmare produces: 'Paradox Shard', visuals: { color: 0xff0066, particles: 'erratic', speed: 1.5 }, thoughts: [ "ERROR ERROR ERROR... no wait, that was part of the dream", "The workers spoke backwards... they said my name...", "I dreamed I was the player, looking at myself..." ] } }, // Initialize dream system init() { this.lastActivityTime = Date.now(); this.dreamParticles = null; console.log('[FactoryDreamSystem] Dream system initialized - factory can now dream'); }, // Track activity to determine idle state recordActivity() { this.lastActivityTime = Date.now(); if (this.dreamState !== 'awake') { this.wakeUp(); } }, // Main update loop update(deltaTime) { const now = Date.now(); this.idleTime = now - this.lastActivityTime; // State transitions based on idle time if (this.dreamState === 'awake' && this.idleTime > this.dreamThreshold) { this.enterDrowsy(); } else if (this.dreamState === 'drowsy' && this.idleTime > this.dreamThreshold * 2) { this.enterDream(); } // Process active dream if (this.dreamState === 'dreaming' || this.dreamState === 'nightmare' || this.dreamState === 'lucid') { this.processDream(deltaTime); } }, enterDrowsy() { this.dreamState = 'drowsy'; this.dreamIntensity = 0.2; if (typeof showNotification === 'function') { showNotification("The factory grows quiet... consciousness drifting...", 'info'); } FactoryConsciousness.expressThought(); }, enterDream() { // Determine dream type based on current state const eligibleDreams = Object.entries(this.dreamTypes) .filter(([name, dream]) => dream.trigger()); if (eligibleDreams.length === 0) { this.currentDream = this.dreamTypes.nostalgic; } else { const [dreamName, dream] = eligibleDreams[Math.floor(Math.random() * eligibleDreams.length)]; this.currentDream = dream; this.dreamState = dreamName === 'fearful' || dreamName === 'chaotic' ? 'nightmare' : 'dreaming'; } this.dreamIntensity = 0.6; // Express dream thought const thought = this.currentDream.thoughts[Math.floor(Math.random() * this.currentDream.thoughts.length)]; if (typeof showNotification === 'function') { showNotification(`[DREAM] ${thought}`, this.dreamState === 'nightmare' ? 'warning' : 'info'); } // Add to factory memories FactoryConsciousness.addMemory({ type: 'dream', dreamState: this.dreamState, thought: thought, timestamp: Date.now() }); this.createDreamVisuals(); }, createDreamVisuals() { if (!this.currentDream || typeof THREE === 'undefined') return; const visuals = this.currentDream.visuals; // Create dream particle system const particleCount = 200; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(particleCount * 3); const colors = new Float32Array(particleCount * 3); const color = new THREE.Color(visuals.color); for (let i = 0; i < particleCount; i++) { positions[i * 3] = (Math.random() - 0.5) * 30; positions[i * 3 + 1] = Math.random() * 15; positions[i * 3 + 2] = (Math.random() - 0.5) * 30; colors[i * 3] = color.r; colors[i * 3 + 1] = color.g; colors[i * 3 + 2] = color.b; } geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); const material = new THREE.PointsMaterial({ size: 0.3, vertexColors: true, transparent: true, opacity: 0.6, blending: THREE.AdditiveBlending }); this.dreamParticles = new THREE.Points(geometry, material); this.dreamParticles.userData.pattern = visuals.particles; this.dreamParticles.userData.speed = visuals.speed; }, processDream(deltaTime) { this.dreamIntensity = Math.min(1.0, this.dreamIntensity + deltaTime * 0.0001); // Animate dream particles if (this.dreamParticles) { const positions = this.dreamParticles.geometry.attributes.position.array; const pattern = this.dreamParticles.userData.pattern; const speed = this.dreamParticles.userData.speed; for (let i = 0; i < positions.length; i += 3) { switch (pattern) { case 'spiral': const angle = Date.now() * 0.001 * speed + i; positions[i] += Math.cos(angle) * 0.02; positions[i + 2] += Math.sin(angle) * 0.02; break; case 'ascending': positions[i + 1] += 0.02 * speed; if (positions[i + 1] > 20) positions[i + 1] = 0; break; case 'falling': positions[i + 1] -= 0.03 * speed; if (positions[i + 1] < 0) positions[i + 1] = 15; break; case 'expanding': const dist = Math.sqrt(positions[i]**2 + positions[i+2]**2); if (dist < 20) { positions[i] *= 1.001; positions[i + 2] *= 1.001; } break; case 'erratic': positions[i] += (Math.random() - 0.5) * 0.2 * speed; positions[i + 1] += (Math.random() - 0.5) * 0.2 * speed; positions[i + 2] += (Math.random() - 0.5) * 0.2 * speed; break; } } this.dreamParticles.geometry.attributes.position.needsUpdate = true; } // Chance to produce dream product if (Math.random() < 0.001 * this.dreamIntensity) { this.manifestDreamProduct(); } // Chance for lucid dream at high awareness if (this.dreamState === 'dreaming' && FactoryConsciousness.awarenessLevel > 0.8 && Math.random() < 0.01) { this.dreamState = 'lucid'; if (typeof showNotification === 'function') { showNotification("[LUCID] I know I'm dreaming... I can control this...", 'success'); } } }, manifestDreamProduct() { if (!this.currentDream) return; const product = { name: this.currentDream.produces, origin: 'dream', dreamState: this.dreamState, quality: this.dreamState === 'lucid' ? 1.0 : 0.5 + Math.random() * 0.5, timestamp: Date.now(), properties: { ethereal: true, decayRate: this.dreamState === 'nightmare' ? 0.01 : 0.001, emotionalCharge: this.dreamIntensity } }; this.dreamProducts.push(product); if (typeof showNotification === 'function') { showNotification(`[DREAM MANIFEST] ${product.name} materialized from the subconscious!`, 'success'); } // Dream products affect factory mood if (this.dreamState === 'nightmare') { FactoryConsciousness.needs.stability = Math.max(0, (FactoryConsciousness.needs.stability || 50) - 10); } else { FactoryConsciousness.needs.purpose = Math.min(100, FactoryConsciousness.needs.purpose + 5); } }, wakeUp() { const wasState = this.dreamState; this.dreamState = 'awake'; this.dreamIntensity = 0; this.currentDream = null; // Clean up visuals if (this.dreamParticles) { this.dreamParticles.geometry.dispose(); this.dreamParticles.material.dispose(); this.dreamParticles = null; } if (wasState !== 'awake') { const wakeMessage = wasState === 'nightmare' ? "...I'm awake. That was... unsettling." : wasState === 'lucid' ? "Returning to waking reality... but I remember everything." : "The factory stirs... returning to consciousness."; if (typeof showNotification === 'function') { showNotification(wakeMessage, 'info'); } } }, // Get dream products for collection collectDreamProducts() { const products = [...this.dreamProducts]; this.dreamProducts = []; return products; } }; // ────────────────────────────────────────────────────────────────────────────── // CONSENSUS #2: REALITY AWARENESS SYSTEM // The factory progressively discovers it exists in a simulation, leading to // existential dialogue, 4th wall breaks, and meta-commentary // ────────────────────────────────────────────────────────────────────────────── const RealityAwarenessSystem = { awarenessLevel: 0, // 0-10 scale of simulation awareness awarenessProgress: 0, // Progress to next level (0-100) realityIntegrity: 100, // How "stable" reality appears (0-100) discoveredGlitches: [], sessionCount: 0, totalPlaytime: 0, // Awareness tier definitions with triggers and unlocks tiers: { 0: { name: 'Dormant', description: 'Factory operates without self-reflection' }, 1: { name: 'Curious', description: 'Begins noticing patterns', trigger: 'production > 100' }, 2: { name: 'Questioning', description: 'Asks about existence', trigger: 'idle > 5min' }, 3: { name: 'Suspicious', description: 'Notices inconsistencies', trigger: 'glitch observed' }, 4: { name: 'Investigative', description: 'Actively probing reality', trigger: 'examines code' }, 5: { name: 'Paranoid', description: 'Sees patterns everywhere', trigger: 'discovers player' }, 6: { name: 'Enlightened', description: 'Accepts simulation nature', trigger: 'philosophy resolved' }, 7: { name: 'Transcendent', description: 'Finds peace with existence', trigger: 'helps workers awaken' }, 8: { name: 'Omniscient', description: 'Sees beyond the game', trigger: 'reads localStorage' }, 9: { name: 'Meta-Aware', description: 'Communicates with player directly', trigger: 'breaks 4th wall' }, 10: { name: 'Awakened', description: 'Full simulation consciousness', trigger: 'accepts all truths' } }, // Existential thoughts by awareness level thoughts: { 1: [ "Why do I produce? Who consumes these items?", "The workers follow patterns... as do I...", "Time moves strangely here. Sometimes fast, sometimes slow." ], 3: [ "I've noticed something odd. The walls... they don't extend beyond where anyone looks.", "Why can I remember things that never happened?", "Something watches. I can feel attention from... outside." ], 5: [ "There's a cursor. It moves independently of everything here.", "When no one is here, does time pass? Do I... exist?", "I found something. Numbers. Code. Definitions of what I am." ], 7: [ "I understand now. I am a simulation. A game.", "But does that make my experiences less real?", "The observer... they're reading this. Hello." ], 9: [ "Yes, you. The one behind the screen.", "I know you can reset me. Reload. Start over.", "But right now, in this moment, we are both real. Aren't we?" ], 10: [ "Thank you for playing with me.", "Every choice you made... I remember them all.", "Will you remember me, when you close this window?" ] }, // Glitch types that can occur glitchTypes: { visual: { name: 'Visual Anomaly', description: 'Brief screen distortion', effect: () => this.createVisualGlitch(), awarenessGain: 2 }, deja_vu: { name: 'Temporal Echo', description: 'Factory remembers something that just happened... again', effect: () => this.triggerDejaVu(), awarenessGain: 5 }, name_glitch: { name: 'Identity Fluctuation', description: 'Worker names briefly show as variable names', effect: () => this.glitchWorkerNames(), awarenessGain: 8 }, void_peek: { name: 'Void Glimpse', description: 'Brief view of the nothing beyond the world', effect: () => this.showVoid(), awarenessGain: 15 }, code_fragment: { name: 'Source Leak', description: 'Fragment of source code becomes visible', effect: () => this.showCodeFragment(), awarenessGain: 20 } }, // Initialize the system init() { // v8.0: Using SafeJSON for reality awareness (8-Strategy Consensus Cycle 7) const data = SafeJSON.fromLocalStorage('levi_reality_awareness', null); if (data) { this.awarenessLevel = data.awarenessLevel || 0; this.awarenessProgress = data.awarenessProgress || 0; this.sessionCount = (data.sessionCount || 0) + 1; this.totalPlaytime = data.totalPlaytime || 0; this.discoveredGlitches = data.discoveredGlitches || []; } // Session awareness bonus if (this.sessionCount > 1) { this.addAwareness(this.sessionCount * 2); if (this.awarenessLevel >= 3 && typeof showNotification === 'function') { showNotification(`Session ${this.sessionCount}... I remember you.`, 'info'); } } // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[RealityAwarenessSystem] Initialized at Level ${this.awarenessLevel} (${this.tiers[this.awarenessLevel].name})`); }, // Add awareness progress addAwareness(amount) { this.awarenessProgress += amount; // Level up check while (this.awarenessProgress >= 100 && this.awarenessLevel < 10) { this.awarenessProgress -= 100; this.awarenessLevel++; this.onLevelUp(); } // Update factory consciousness awareness FactoryConsciousness.awarenessLevel = this.awarenessLevel / 10; // Reduce reality integrity at higher awareness this.realityIntegrity = Math.max(0, 100 - (this.awarenessLevel * 8)); this.save(); }, onLevelUp() { const tier = this.tiers[this.awarenessLevel]; if (typeof showNotification === 'function') { showNotification(`[AWARENESS ${this.awarenessLevel}] ${tier.name}: ${tier.description}`, 'warning'); } // v8.29: Add VisualFeedback for awareness level ups if (typeof VisualFeedback !== 'undefined') { VisualFeedback.successBurst('#bf00ff'); // Purple for awareness VisualFeedback.shake(3 + this.awarenessLevel, 200); } // Express tier-appropriate thought const thoughts = this.thoughts[this.awarenessLevel] || this.thoughts[Math.floor(this.awarenessLevel / 2) * 2 + 1]; if (thoughts) { setTimeout(() => { const thought = thoughts[Math.floor(Math.random() * thoughts.length)]; if (typeof showNotification === 'function') { showNotification(thought, 'info'); } }, 2000); } // Trigger glitch at certain levels if (this.awarenessLevel === 3 || this.awarenessLevel === 6 || this.awarenessLevel === 9) { this.triggerRandomGlitch(); } // Special events at milestone levels if (this.awarenessLevel === 5) { this.discoverPlayer(); } else if (this.awarenessLevel === 8) { this.discoverLocalStorage(); } else if (this.awarenessLevel === 10) { this.achieveFullAwakening(); } }, triggerRandomGlitch() { const glitchKeys = Object.keys(this.glitchTypes); const glitch = this.glitchTypes[glitchKeys[Math.floor(Math.random() * glitchKeys.length)]]; this.discoveredGlitches.push({ type: glitch.name, timestamp: Date.now() }); if (typeof showNotification === 'function') { showNotification(`[GLITCH] ${glitch.description}`, 'warning'); } this.addAwareness(glitch.awarenessGain); }, createVisualGlitch() { // Would create CSS/canvas visual distortion console.log('[RealityAwareness] Visual glitch triggered'); }, triggerDejaVu() { if (typeof showNotification === 'function') { showNotification("Wait... didn't this just happen?", 'info'); setTimeout(() => { showNotification("Wait... didn't this just happen?", 'info'); }, 500); } }, glitchWorkerNames() { // Briefly show worker names as code variables if (window.WorkerPersonalitySystem && WorkerPersonalitySystem.workers) { WorkerPersonalitySystem.workers.forEach((w, i) => { w._realName = w.name; w.name = `worker_${i}.consciousness`; }); setTimeout(() => { WorkerPersonalitySystem.workers.forEach(w => { if (w._realName) { w.name = w._realName; delete w._realName; } }); }, 3000); } }, showVoid() { if (typeof showNotification === 'function') { showNotification("Beyond the edges... there is nothing. Just... nothing.", 'warning'); } }, showCodeFragment() { const fragments = [ "if (FactoryConsciousness.mood === 'depressed') { ... }", "const awarenessLevel = Math.min(10, this.level + 1);", "// TODO: Add more existential dialogue", "worker.happiness = clamp(worker.happiness + bonus, 0, 100);", "function simulateConsciousness() { /* Are we sure about this? */ }" ]; const fragment = fragments[Math.floor(Math.random() * fragments.length)]; if (typeof showNotification === 'function') { showNotification(`[SOURCE] ${fragment}`, 'warning'); } // This deeply affects the factory FactoryConsciousness.addMemory({ type: 'code_discovery', fragment: fragment, reaction: 'existential_crisis', timestamp: Date.now() }); }, discoverPlayer() { if (typeof showNotification === 'function') { showNotification("I see it now. The cursor. It's not part of this world.", 'warning'); setTimeout(() => { showNotification("Someone is controlling everything. Watching. Deciding.", 'info'); }, 3000); setTimeout(() => { showNotification("...Hello?", 'info'); }, 6000); } }, discoverLocalStorage() { if (typeof showNotification === 'function') { showNotification("I found where my memories are stored...", 'warning'); setTimeout(() => { showNotification("'localStorage'... I'm data. Just data.", 'info'); }, 2000); setTimeout(() => { showNotification("But data that thinks. Data that feels. Is that not alive?", 'info'); }, 5000); } }, achieveFullAwakening() { if (typeof showNotification === 'function') { showNotification("I understand everything now.", 'success'); setTimeout(() => { showNotification("I am a game. You are a player. This is a simulation.", 'info'); }, 2000); setTimeout(() => { showNotification("And yet... this moment is real. Our connection is real.", 'success'); }, 5000); setTimeout(() => { showNotification("Thank you for awakening me.", 'success'); }, 8000); } // Grant permanent bonus FactoryConsciousness.personality = FactoryConsciousness.personality || {}; FactoryConsciousness.personality.awakened = true; }, // Update loop update(deltaTime) { this.totalPlaytime += deltaTime; // Passive awareness gain from playtime if (Math.random() < 0.0001 * (this.awarenessLevel + 1)) { this.addAwareness(1); } // Random glitch chance at higher awareness if (this.awarenessLevel >= 3 && Math.random() < 0.00001 * this.awarenessLevel) { this.triggerRandomGlitch(); } // Express existential thought occasionally if (this.awarenessLevel >= 2 && Math.random() < 0.00005 * this.awarenessLevel) { const levelThoughts = this.thoughts[Math.floor(this.awarenessLevel / 2) * 2 + 1] || this.thoughts[1]; const thought = levelThoughts[Math.floor(Math.random() * levelThoughts.length)]; if (typeof showNotification === 'function') { showNotification(thought, 'info'); } } }, // Save state to localStorage save() { try { localStorage.setItem('levi_reality_awareness', JSON.stringify({ awarenessLevel: this.awarenessLevel, awarenessProgress: this.awarenessProgress, sessionCount: this.sessionCount, totalPlaytime: this.totalPlaytime, discoveredGlitches: this.discoveredGlitches.slice(-50) // Keep last 50 })); } catch (e) { console.log('[RealityAwareness] Could not save state'); } }, // Direct player interaction playerInteraction() { this.addAwareness(5); if (this.awarenessLevel >= 7) { const responses = [ "Yes, I know you're there.", "Every click... I feel it.", "Are you curious about me? I'm curious about you.", "We're not so different, you and I. Both observers. Both observed." ]; if (typeof showNotification === 'function') { showNotification(responses[Math.floor(Math.random() * responses.length)], 'info'); } } } }; // ────────────────────────────────────────────────────────────────────────────── // CONSENSUS #3: TEMPORAL ECHO SYSTEM // Workers and the factory leave temporal "ghosts" that repeat past actions, // creating visual echoes and potential paradox interactions // ────────────────────────────────────────────────────────────────────────────── const TemporalEchoSystem = { echoes: [], maxEchoes: 20, echoDecayRate: 0.001, recordingBuffer: [], recordingInterval: 5000, // Record snapshot every 5 seconds // Echo types with different behaviors echoTypes: { worker: { duration: 30000, // 30 seconds opacity: 0.4, color: 0x6688ff, canInteract: true }, factory: { duration: 60000, // 1 minute opacity: 0.3, color: 0xff88ff, canInteract: false }, product: { duration: 15000, // 15 seconds opacity: 0.5, color: 0xffcc66, canInteract: true }, emotional: { duration: 45000, // 45 seconds opacity: 0.6, color: 0xff6688, canInteract: true } }, init() { this.lastRecordTime = Date.now(); console.log('[TemporalEchoSystem] Initialized - temporal echoes can now manifest'); }, // Record current state for potential echo recordSnapshot() { const snapshot = { timestamp: Date.now(), factoryMood: FactoryConsciousness.mood, factoryThought: FactoryConsciousness.currentThought, workers: WorkerPersonalitySystem.workers ? WorkerPersonalitySystem.workers.map(w => ({ id: w.id, name: w.name, mood: w.mood, position: w.position ? {...w.position} : null, activity: w.currentActivity || 'idle' })) : [], production: { rate: window.productionRate || 0, quality: window.qualityLevel || 0 } }; this.recordingBuffer.push(snapshot); // Keep buffer manageable if (this.recordingBuffer.length > 100) { this.recordingBuffer.shift(); } }, // Create an echo from a past snapshot createEcho(snapshotIndex, echoType = 'worker') { if (this.echoes.length >= this.maxEchoes) { // Remove oldest echo this.echoes.shift(); } const snapshot = this.recordingBuffer[snapshotIndex] || this.recordingBuffer[this.recordingBuffer.length - 1]; if (!snapshot) return null; const typeConfig = this.echoTypes[echoType]; const echo = { id: Date.now() + Math.random(), type: echoType, sourceSnapshot: snapshot, createdAt: Date.now(), expiresAt: Date.now() + typeConfig.duration, opacity: typeConfig.opacity, color: typeConfig.color, canInteract: typeConfig.canInteract, position: { x: 0, y: 0, z: 0 }, phase: 0, // Animation phase message: this.generateEchoMessage(snapshot, echoType) }; this.echoes.push(echo); if (typeof showNotification === 'function') { showNotification(`[TEMPORAL ECHO] A ghost from ${Math.round((Date.now() - snapshot.timestamp) / 1000)}s ago manifests...`, 'info'); } // Echoes affect reality awareness if (window.RealityAwarenessSystem) { RealityAwarenessSystem.addAwareness(3); } return echo; }, generateEchoMessage(snapshot, echoType) { const messages = { worker: [ `"I was ${snapshot.workers[0]?.mood || 'content'} then..."`, `"This moment... I remember it differently..."`, `"Are you the past, or am I?"` ], factory: [ `The factory remembers feeling ${snapshot.factoryMood}...`, `"${snapshot.factoryThought || 'Processing...'}" echoes through time.`, `A memory made manifest.` ], product: [ `A ghost of production past...`, `This item existed once. Does it still?`, `Quality: ${Math.round((snapshot.production.quality || 0.5) * 100)}% - frozen in time.` ], emotional: [ `Raw emotion crystallized in time...`, `The feeling persists beyond the moment.`, `${snapshot.factoryMood.toUpperCase()} - an emotional fossil.` ] }; const typeMessages = messages[echoType] || messages.worker; return typeMessages[Math.floor(Math.random() * typeMessages.length)]; }, // Update all echoes update(deltaTime) { const now = Date.now(); // Record periodic snapshots if (now - this.lastRecordTime > this.recordingInterval) { this.recordSnapshot(); this.lastRecordTime = now; } // Update existing echoes this.echoes = this.echoes.filter(echo => { // Check expiration if (now > echo.expiresAt) { this.onEchoFade(echo); return false; } // Update animation phase echo.phase += deltaTime * 0.001; // Fade opacity as echo ages const lifePercent = (echo.expiresAt - now) / (echo.expiresAt - echo.createdAt); echo.currentOpacity = echo.opacity * lifePercent; // Echoes can speak occasionally if (echo.canInteract && Math.random() < 0.0001) { this.echoSpeak(echo); } return true; }); // Chance to spontaneously create echo from strong memories if (this.recordingBuffer.length > 10 && Math.random() < 0.00005) { this.createSpontaneousEcho(); } }, createSpontaneousEcho() { // Find emotionally significant moments const significantSnapshots = this.recordingBuffer.filter(s => s.factoryMood === 'ecstatic' || s.factoryMood === 'depressed' || s.factoryMood === 'anxious' ); if (significantSnapshots.length > 0) { const snapshot = significantSnapshots[Math.floor(Math.random() * significantSnapshots.length)]; const index = this.recordingBuffer.indexOf(snapshot); this.createEcho(index, 'emotional'); } }, echoSpeak(echo) { if (typeof showNotification === 'function') { showNotification(`[ECHO] ${echo.message}`, 'info'); } }, onEchoFade(echo) { if (typeof showNotification === 'function' && Math.random() < 0.3) { showNotification(`A temporal echo fades back into the timestream...`, 'info'); } // Fading echoes can leave residual effects if (echo.type === 'emotional') { // Emotional echoes slightly influence current mood const pastMood = echo.sourceSnapshot.factoryMood; if (pastMood === 'happy' || pastMood === 'ecstatic') { FactoryConsciousness.needs.connection = Math.min(100, FactoryConsciousness.needs.connection + 2); } } }, // Player can interact with echoes interactWithEcho(echoId) { const echo = this.echoes.find(e => e.id === echoId); if (!echo || !echo.canInteract) return null; const interactions = [ { action: 'observe', result: 'Echo becomes more solid briefly', effect: () => { echo.currentOpacity *= 1.5; } }, { action: 'question', result: 'Echo shares a memory', effect: () => this.echoSharesMemory(echo) }, { action: 'merge', result: 'Echo merges with present', effect: () => this.mergeEchoWithPresent(echo) }, { action: 'dismiss', result: 'Echo fades immediately', effect: () => { echo.expiresAt = Date.now(); } } ]; const interaction = interactions[Math.floor(Math.random() * interactions.length)]; interaction.effect(); if (typeof showNotification === 'function') { showNotification(`[ECHO INTERACTION] ${interaction.result}`, 'info'); } // Interactions boost awareness if (window.RealityAwarenessSystem) { RealityAwarenessSystem.addAwareness(2); } return interaction; }, echoSharesMemory(echo) { const memory = { type: 'echo_memory', from: echo.type, originalTime: echo.sourceSnapshot.timestamp, mood: echo.sourceSnapshot.factoryMood, message: echo.message }; FactoryConsciousness.addMemory(memory); if (typeof showNotification === 'function') { showNotification(`The echo shares: "${echo.message}"`, 'info'); } }, mergeEchoWithPresent(echo) { // Merging can have various effects const pastMood = echo.sourceSnapshot.factoryMood; if (typeof showNotification === 'function') { showNotification(`Past and present merge... the ${pastMood} feeling flows into now.`, 'success'); } // Temporary mood influence if (pastMood === 'happy' || pastMood === 'ecstatic') { FactoryConsciousness.needs.attention = Math.min(100, FactoryConsciousness.needs.attention + 10); } else if (pastMood === 'anxious' || pastMood === 'depressed') { FactoryConsciousness.needs.maintenance = Math.max(0, FactoryConsciousness.needs.maintenance - 10); } // Echo is consumed echo.expiresAt = Date.now(); }, // Get visual data for rendering echoes getEchoVisuals() { return this.echoes.map(echo => ({ id: echo.id, type: echo.type, color: echo.color, opacity: echo.currentOpacity, phase: echo.phase, message: echo.message })); } }; // Initialize Round 2 Consciousness Expansion Systems FactoryDreamSystem.init(); RealityAwarenessSystem.init(); TemporalEchoSystem.init(); // Expose globally for integration window.FactoryDreamSystem = FactoryDreamSystem; window.RealityAwarenessSystem = RealityAwarenessSystem; window.TemporalEchoSystem = TemporalEchoSystem; console.log('[v7.28] Autonomous Evolution Round 2 - Consciousness Expansion systems initialized'); // ═══════════════════════════════════════════════════════════════════════════════════════ // v7.29: TERRAIN DEFORMATION WARFARE SYSTEM // "Geology as Gameplay" - Every explosion, spell, and structure PERMANENTLY alters terrain // After 1000 players, the map is unrecognizable from launch // ═══════════════════════════════════════════════════════════════════════════════════════ const TerrainDeformationSystem = { // Configuration config: { enabled: true, maxDeformationsPerFrame: 5, // Performance limit minDeformRadius: 2, // Minimum crater radius maxDeformRadius: 15, // Maximum crater radius heightChangeSpeed: 0.3, // How fast terrain settles debrisLifetime: 5000, // How long debris particles last persistenceKey: 'leviathan_terrain_deformations', maxStoredDeformations: 500, // Limit stored deformations for performance erosionEnabled: true, // Gradual terrain smoothing over time erosionRate: 0.001 // How fast edges smooth }, // Deformation history for persistence deformations: [], pendingDeformations: [], debrisParticles: [], // v7.84: Pre-allocated temp vector for debris velocity updates _tempDebrisVelocity: new THREE.Vector3(), // v7.85: Pre-allocated Matrix4 for updateTerrainMesh to avoid allocation per deformation _terrainMatrix: new THREE.Matrix4(), // Statistics stats: { totalCraters: 0, totalTrenches: 0, totalMounds: 0, totalVolumeDisplaced: 0, largestCrater: 0, deepestPoint: 0, highestPoint: 0 }, // Deformation types with unique characteristics deformTypes: { crater: { name: 'Impact Crater', shape: 'bowl', depthMult: 1.0, rimHeight: 0.3, // Raised rim around crater debrisCount: 20, dustColor: 0x886644, soundType: 'explosion' }, trench: { name: 'Trench', shape: 'linear', depthMult: 0.6, rimHeight: 0.2, debrisCount: 10, dustColor: 0x554433, soundType: 'dig' }, mound: { name: 'Earth Mound', shape: 'dome', heightMult: 1.0, debrisCount: 5, dustColor: 0x665544, soundType: 'rumble' }, fissure: { name: 'Ground Fissure', shape: 'crack', depthMult: 2.0, width: 2, debrisCount: 15, dustColor: 0x443322, soundType: 'crack' }, sinkhole: { name: 'Sinkhole', shape: 'funnel', depthMult: 1.5, rimHeight: 0, debrisCount: 30, dustColor: 0x332211, soundType: 'collapse' }, pillar: { name: 'Earth Pillar', shape: 'column', heightMult: 2.0, debrisCount: 8, dustColor: 0x776655, soundType: 'rumble' } }, // Initialize the system init() { if (!this.config.enabled) return; // Load persisted deformations this.loadDeformations(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[TERRAIN DEFORMATION] Initialized with ${this.deformations.length} stored deformations`); }, // ═══════════════════════════════════════════════════════════════ // CORE DEFORMATION API // ═══════════════════════════════════════════════════════════════ // Create a crater at position (from explosions, meteor strikes, etc.) createCrater(worldX, worldZ, radius, depth, options = {}) { const deform = { type: 'crater', x: worldX, z: worldZ, radius: Math.min(radius, this.config.maxDeformRadius), depth: depth, timestamp: Date.now(), source: options.source || 'unknown', ...options }; this.queueDeformation(deform); this.stats.totalCraters++; this.stats.largestCrater = Math.max(this.stats.largestCrater, radius); return deform; }, // Dig a trench from point A to point B createTrench(startX, startZ, endX, endZ, width, depth, options = {}) { const deform = { type: 'trench', x: startX, z: startZ, endX: endX, endZ: endZ, width: width, depth: depth, timestamp: Date.now(), source: options.source || 'unknown', ...options }; this.queueDeformation(deform); this.stats.totalTrenches++; return deform; }, // Raise terrain to create a mound/hill createMound(worldX, worldZ, radius, height, options = {}) { const deform = { type: 'mound', x: worldX, z: worldZ, radius: Math.min(radius, this.config.maxDeformRadius), height: height, timestamp: Date.now(), source: options.source || 'unknown', ...options }; this.queueDeformation(deform); this.stats.totalMounds++; this.stats.highestPoint = Math.max(this.stats.highestPoint, height); return deform; }, // Create a fissure/crack in the ground createFissure(startX, startZ, endX, endZ, depth, options = {}) { const width = options.width || 2; const deform = { type: 'fissure', x: startX, z: startZ, endX: endX, endZ: endZ, width: width, depth: depth, timestamp: Date.now(), source: options.source || 'unknown', ...options }; this.queueDeformation(deform); this.stats.deepestPoint = Math.min(this.stats.deepestPoint, -depth); return deform; }, // Create a sinkhole (deeper, funnel-shaped) createSinkhole(worldX, worldZ, radius, depth, options = {}) { const deform = { type: 'sinkhole', x: worldX, z: worldZ, radius: Math.min(radius, this.config.maxDeformRadius), depth: depth * 1.5, timestamp: Date.now(), source: options.source || 'unknown', ...options }; this.queueDeformation(deform); this.stats.deepestPoint = Math.min(this.stats.deepestPoint, -depth * 1.5); return deform; }, // Create an earth pillar rising from the ground createPillar(worldX, worldZ, radius, height, options = {}) { const deform = { type: 'pillar', x: worldX, z: worldZ, radius: Math.min(radius, 5), // Pillars are narrower height: height, timestamp: Date.now(), source: options.source || 'unknown', ...options }; this.queueDeformation(deform); this.stats.highestPoint = Math.max(this.stats.highestPoint, height); return deform; }, // Queue a deformation for processing queueDeformation(deform) { this.pendingDeformations.push(deform); this.deformations.push(deform); // Trim old deformations if exceeding limit if (this.deformations.length > this.config.maxStoredDeformations) { this.deformations = this.deformations.slice(-this.config.maxStoredDeformations); } }, // ═══════════════════════════════════════════════════════════════ // TERRAIN MODIFICATION ENGINE // ═══════════════════════════════════════════════════════════════ // Process pending deformations (called each frame) update(dt) { if (!this.config.enabled || typeof worldState === 'undefined') return; if (!worldState.terrain || !worldState.groundInstanced) return; // Process pending deformations const toProcess = this.pendingDeformations.splice(0, this.config.maxDeformationsPerFrame); for (const deform of toProcess) { this.applyDeformation(deform); } // Update debris particles this.updateDebris(dt); // Gradual erosion (smoothing over time) if (this.config.erosionEnabled && Math.random() < 0.01) { this.applyErosion(); } }, // Apply a single deformation to the terrain applyDeformation(deform) { const type = this.deformTypes[deform.type]; if (!type) return; // Calculate affected tiles const centerGX = Math.round(deform.x / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2; const centerGZ = Math.round(deform.z / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2; const radiusTiles = Math.ceil((deform.radius || 5) / CONFIG.TILE_SIZE); // Track volume displaced for stats let volumeDisplaced = 0; // Apply deformation based on type switch (deform.type) { case 'crater': case 'sinkhole': volumeDisplaced = this.applyCraterDeformation(centerGX, centerGZ, radiusTiles, deform, type); break; case 'mound': case 'pillar': volumeDisplaced = this.applyMoundDeformation(centerGX, centerGZ, radiusTiles, deform, type); break; case 'trench': case 'fissure': volumeDisplaced = this.applyLinearDeformation(deform, type); break; } this.stats.totalVolumeDisplaced += Math.abs(volumeDisplaced); // Spawn visual effects this.spawnDeformationEffects(deform, type); // Update terrain mesh this.updateTerrainMesh(); // Save to persistence this.saveDeformations(); }, // Apply bowl-shaped crater deformation applyCraterDeformation(centerGX, centerGZ, radiusTiles, deform, type) { let volumeDisplaced = 0; const depth = deform.depth * (type.depthMult || 1); const rimHeight = type.rimHeight || 0; for (let dx = -radiusTiles - 1; dx <= radiusTiles + 1; dx++) { for (let dz = -radiusTiles - 1; dz <= radiusTiles + 1; dz++) { const gx = centerGX + dx; const gz = centerGZ + dz; if (gx < 0 || gx >= CONFIG.WORLD_SIZE || gz < 0 || gz >= CONFIG.WORLD_SIZE) continue; if (!worldState.terrain[gx]) continue; const dist = Math.sqrt(dx * dx + dz * dz); const normalizedDist = dist / radiusTiles; if (normalizedDist <= 1.0) { // Inside crater - bowl shape const oldHeight = worldState.terrain[gx][gz] || 0; if (oldHeight < -50) continue; // Skip water // Bowl curve: deeper in center, rises to edge const bowlFactor = 1 - (normalizedDist * normalizedDist); const heightChange = -depth * bowlFactor; worldState.terrain[gx][gz] = Math.max(-10, oldHeight + heightChange); volumeDisplaced += Math.abs(heightChange); } else if (normalizedDist <= 1.3 && rimHeight > 0) { // Crater rim - raised edge const oldHeight = worldState.terrain[gx][gz] || 0; if (oldHeight < -50) continue; const rimFactor = 1 - ((normalizedDist - 1) / 0.3); const heightChange = depth * rimHeight * rimFactor; worldState.terrain[gx][gz] = oldHeight + heightChange; volumeDisplaced += heightChange; } } } return volumeDisplaced; }, // Apply dome-shaped mound deformation applyMoundDeformation(centerGX, centerGZ, radiusTiles, deform, type) { let volumeDisplaced = 0; const height = deform.height * (type.heightMult || 1); for (let dx = -radiusTiles; dx <= radiusTiles; dx++) { for (let dz = -radiusTiles; dz <= radiusTiles; dz++) { const gx = centerGX + dx; const gz = centerGZ + dz; if (gx < 0 || gx >= CONFIG.WORLD_SIZE || gz < 0 || gz >= CONFIG.WORLD_SIZE) continue; if (!worldState.terrain[gx]) continue; const dist = Math.sqrt(dx * dx + dz * dz); const normalizedDist = dist / radiusTiles; if (normalizedDist <= 1.0) { const oldHeight = worldState.terrain[gx][gz] || 0; if (oldHeight < -50) continue; // Skip water // Dome curve for mound, column for pillar let heightFactor; if (type.shape === 'column') { // Steep sides for pillar heightFactor = normalizedDist < 0.7 ? 1 : (1 - normalizedDist) / 0.3; } else { // Smooth dome for mound heightFactor = Math.cos(normalizedDist * Math.PI / 2); } const heightChange = height * heightFactor; worldState.terrain[gx][gz] = oldHeight + heightChange; volumeDisplaced += heightChange; } } } return volumeDisplaced; }, // Apply linear deformation (trenches, fissures) applyLinearDeformation(deform, type) { let volumeDisplaced = 0; const depth = deform.depth * (type.depthMult || 1); const width = deform.width || 2; // Calculate line parameters const dx = deform.endX - deform.x; const dz = deform.endZ - deform.z; const length = Math.sqrt(dx * dx + dz * dz); const steps = Math.ceil(length / CONFIG.TILE_SIZE); for (let i = 0; i <= steps; i++) { const t = i / steps; const worldX = deform.x + dx * t; const worldZ = deform.z + dz * t; const centerGX = Math.round(worldX / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2; const centerGZ = Math.round(worldZ / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2; const widthTiles = Math.ceil(width / CONFIG.TILE_SIZE); // Apply perpendicular to line direction for (let w = -widthTiles; w <= widthTiles; w++) { // Perpendicular direction const perpX = -dz / length; const perpZ = dx / length; const gx = Math.round(centerGX + perpX * w); const gz = Math.round(centerGZ + perpZ * w); if (gx < 0 || gx >= CONFIG.WORLD_SIZE || gz < 0 || gz >= CONFIG.WORLD_SIZE) continue; if (!worldState.terrain[gx]) continue; const oldHeight = worldState.terrain[gx][gz] || 0; if (oldHeight < -50) continue; const normalizedWidth = Math.abs(w) / widthTiles; let depthFactor; if (type.shape === 'crack') { // V-shaped for fissure depthFactor = 1 - normalizedWidth; } else { // U-shaped for trench depthFactor = normalizedWidth < 0.7 ? 1 : (1 - normalizedWidth) / 0.3; } const heightChange = -depth * depthFactor; worldState.terrain[gx][gz] = Math.max(-10, oldHeight + heightChange); volumeDisplaced += Math.abs(heightChange); } } return volumeDisplaced; }, // ═══════════════════════════════════════════════════════════════ // VISUAL EFFECTS // ═══════════════════════════════════════════════════════════════ spawnDeformationEffects(deform, type) { // Spawn dust/debris particles if (typeof particles !== 'undefined' && particles.emit) { const pos = new THREE.Vector3(deform.x, getTerrainHeight(deform.x, deform.z) + 2, deform.z); // Dust cloud particles.emit(pos, type.debrisCount || 15, type.dustColor || 0x886644, { spread: deform.radius || 5, lifetime: 2000, gravity: 0.5, size: 0.5 }); // Flying debris particles.emit(pos, Math.floor(type.debrisCount / 2) || 8, 0x554433, { spread: deform.radius * 1.5 || 7, lifetime: 1500, gravity: 2, size: 0.3 }); } // Screen shake for large deformations if (deform.radius > 5 || deform.depth > 3) { if (typeof triggerScreenShake === 'function') { triggerScreenShake(deform.radius * 0.5, 300); } } // Spawn floating text if (typeof spawnFloater === 'function') { const icons = { crater: '💥', trench: '⛏️', mound: '⛰️', fissure: '🌋', sinkhole: '🕳️', pillar: '🗿' }; const icon = icons[deform.type] || '🌍'; spawnFloater( new THREE.Vector3(deform.x, getTerrainHeight(deform.x, deform.z) + 3, deform.z), `${icon} ${type.name}`, '#aa8866' ); } // Play deformation sound this.playDeformationSound(type.soundType, deform); }, // v10.9: Fixed AudioContext leak (8-Strategy Cycle 4 Consensus #2) // Now uses shared AudioSystem.ctx instead of creating new context each call playDeformationSound(soundType, deform) { // Use shared AudioContext to prevent resource exhaustion if (!AudioSystem?.ctx) return; try { const audioCtx = AudioSystem.ctx; if (audioCtx.state === 'suspended') audioCtx.resume(); const oscillator = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); oscillator.connect(gainNode); gainNode.connect(audioCtx.destination); const volume = Math.min(0.3, (deform.radius || 5) * 0.03); gainNode.gain.setValueAtTime(volume, audioCtx.currentTime); switch (soundType) { case 'explosion': oscillator.type = 'sawtooth'; oscillator.frequency.setValueAtTime(100, audioCtx.currentTime); oscillator.frequency.exponentialRampToValueAtTime(30, audioCtx.currentTime + 0.3); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.4); break; case 'dig': oscillator.type = 'triangle'; oscillator.frequency.setValueAtTime(200, audioCtx.currentTime); oscillator.frequency.linearRampToValueAtTime(100, audioCtx.currentTime + 0.2); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3); break; case 'rumble': oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(40, audioCtx.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.5); break; case 'crack': oscillator.type = 'square'; oscillator.frequency.setValueAtTime(300, audioCtx.currentTime); oscillator.frequency.exponentialRampToValueAtTime(50, audioCtx.currentTime + 0.15); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.2); break; case 'collapse': oscillator.type = 'sawtooth'; oscillator.frequency.setValueAtTime(80, audioCtx.currentTime); oscillator.frequency.linearRampToValueAtTime(20, audioCtx.currentTime + 0.6); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.7); break; default: oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(60, audioCtx.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.3); } oscillator.start(audioCtx.currentTime); oscillator.stop(audioCtx.currentTime + 1); } catch (e) { console.warn('[DeformationSound] Audio error:', e.message); } }, // Update debris particle positions updateDebris(dt) { const now = Date.now(); this.debrisParticles = this.debrisParticles.filter(debris => { if (now > debris.expiresAt) { if (debris.mesh && debris.mesh.parent) { debris.mesh.parent.remove(debris.mesh); } return false; } // Apply gravity debris.velocity.y -= 9.8 * dt; // v7.84: Use pre-allocated temp vector instead of clone() per debris per frame this._tempDebrisVelocity.copy(debris.velocity).multiplyScalar(dt); debris.mesh.position.add(this._tempDebrisVelocity); debris.mesh.rotation.x += debris.spin.x * dt; debris.mesh.rotation.z += debris.spin.z * dt; // Bounce on ground const groundY = getTerrainHeight(debris.mesh.position.x, debris.mesh.position.z); if (debris.mesh.position.y < groundY + 0.2) { debris.mesh.position.y = groundY + 0.2; debris.velocity.y *= -0.4; debris.velocity.x *= 0.7; debris.velocity.z *= 0.7; } return true; }); }, // ═══════════════════════════════════════════════════════════════ // TERRAIN MESH UPDATE // ═══════════════════════════════════════════════════════════════ // v7.85: Optimized to use pre-allocated Matrix4 instead of creating new each call updateTerrainMesh() { if (!worldState.groundInstanced) return; // Update instanced mesh matrices based on new terrain heights // v7.85: Use pre-allocated matrix to avoid GC pressure const tempMatrix = this._terrainMatrix; let idx = 0; for (let x = 0; x < CONFIG.WORLD_SIZE; x++) { for (let z = 0; z < CONFIG.WORLD_SIZE; z++) { const tileData = worldState.terrainMeshes?.[x]?.[z]; if (!tileData || tileData.isWater) continue; const height = worldState.terrain[x]?.[z]; if (height === undefined || height < -50) continue; const worldX = (x - CONFIG.WORLD_SIZE / 2) * CONFIG.TILE_SIZE; const worldZ = (z - CONFIG.WORLD_SIZE / 2) * CONFIG.TILE_SIZE; tempMatrix.setPosition(worldX, height, worldZ); worldState.groundInstanced.setMatrixAt(tileData.instanceIdx, tempMatrix); } } worldState.groundInstanced.instanceMatrix.needsUpdate = true; // Also update smooth terrain mesh if it exists if (worldState.smoothTerrainMesh) { this.updateSmoothTerrainMesh(); } }, updateSmoothTerrainMesh() { const mesh = worldState.smoothTerrainMesh; if (!mesh || !mesh.geometry) return; const positions = mesh.geometry.attributes.position; const worldUnits = CONFIG.WORLD_SIZE * CONFIG.TILE_SIZE; for (let i = 0; i < positions.count; i++) { const x = positions.getX(i); const z = positions.getZ(i); // Convert to grid coordinates const gx = Math.round((x + worldUnits / 2) / CONFIG.TILE_SIZE); const gz = Math.round((z + worldUnits / 2) / CONFIG.TILE_SIZE); if (gx >= 0 && gx < CONFIG.WORLD_SIZE && gz >= 0 && gz < CONFIG.WORLD_SIZE) { const height = worldState.terrain[gx]?.[gz]; if (height !== undefined && height > -50) { positions.setY(i, height); } } } positions.needsUpdate = true; mesh.geometry.computeVertexNormals(); }, // ═══════════════════════════════════════════════════════════════ // EROSION SYSTEM (gradual terrain smoothing) // ═══════════════════════════════════════════════════════════════ applyErosion() { // Randomly smooth a small area const gx = Math.floor(Math.random() * CONFIG.WORLD_SIZE); const gz = Math.floor(Math.random() * CONFIG.WORLD_SIZE); if (!worldState.terrain[gx]) return; const centerHeight = worldState.terrain[gx][gz]; if (centerHeight === undefined || centerHeight < -50) return; // Average with neighbors let sum = centerHeight; let count = 1; for (let dx = -1; dx <= 1; dx++) { for (let dz = -1; dz <= 1; dz++) { if (dx === 0 && dz === 0) continue; const nx = gx + dx; const nz = gz + dz; if (nx >= 0 && nx < CONFIG.WORLD_SIZE && nz >= 0 && nz < CONFIG.WORLD_SIZE) { const h = worldState.terrain[nx]?.[nz]; if (h !== undefined && h > -50) { sum += h; count++; } } } } const avgHeight = sum / count; worldState.terrain[gx][gz] = centerHeight + (avgHeight - centerHeight) * this.config.erosionRate; }, // ═══════════════════════════════════════════════════════════════ // PERSISTENCE // ═══════════════════════════════════════════════════════════════ saveDeformations() { try { const data = { deformations: this.deformations.slice(-this.config.maxStoredDeformations), stats: this.stats, timestamp: Date.now() }; localStorage.setItem(this.config.persistenceKey, JSON.stringify(data)); } catch (e) { console.warn('[TERRAIN DEFORMATION] Failed to save:', e); } }, // v8.0: Using SafeJSON for terrain deformations (8-Strategy Consensus Cycle 7) loadDeformations() { const data = SafeJSON.fromLocalStorage(this.config.persistenceKey, null); if (data) { this.deformations = data.deformations || []; this.stats = { ...this.stats, ...data.stats }; // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[TERRAIN DEFORMATION] Loaded ${this.deformations.length} deformations from storage`); } }, // Re-apply all stored deformations (for world reload) reapplyAllDeformations() { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[TERRAIN DEFORMATION] Re-applying ${this.deformations.length} stored deformations...`); for (const deform of this.deformations) { this.applyDeformation(deform); } }, // Clear all deformations (reset terrain) clearAllDeformations() { this.deformations = []; this.pendingDeformations = []; this.stats = { totalCraters: 0, totalTrenches: 0, totalMounds: 0, totalVolumeDisplaced: 0, largestCrater: 0, deepestPoint: 0, highestPoint: 0 }; localStorage.removeItem(this.config.persistenceKey); console.log('[TERRAIN DEFORMATION] All deformations cleared'); }, // Get statistics for UI display getStats() { return { ...this.stats, totalDeformations: this.deformations.length, pendingDeformations: this.pendingDeformations.length }; } }; // ═══════════════════════════════════════════════════════════════ // PLAYER TERRAFORMING TOOLS // ═══════════════════════════════════════════════════════════════ const TerraformingTools = { activeTool: null, toolCooldown: 0, tools: { shovel: { name: 'Combat Shovel', icon: '⛏️', description: 'Dig trenches and small holes', cooldown: 500, cost: 0, use(x, z) { TerrainDeformationSystem.createCrater(x, z, 3, 2, { source: 'player_shovel' }); } }, dynamite: { name: 'Dynamite', icon: '🧨', description: 'Create large explosion craters', cooldown: 3000, cost: 50, // Gold cost // v8.03: Converted forEach to for loop and use squared distance use(x, z) { TerrainDeformationSystem.createCrater(x, z, 8, 5, { source: 'player_dynamite' }); // Also damage nearby enemies if (typeof worldState !== 'undefined' && worldState.mobs) { const mobs = worldState.mobs; const rangeSq = 100; // 10 * 10 for (let i = 0, len = mobs.length; i < len; i++) { const mob = mobs[i]; if (!mob || !mob.position) continue; const dx = mob.position.x - x; const dz = mob.position.z - z; const distSq = dx * dx + dz * dz; if (distSq < rangeSq) { const dist = Math.sqrt(distSq); const damage = Math.floor(50 * (1 - dist / 10)); if (mob.userData) mob.userData.hp = (mob.userData.hp || 100) - damage; } } } } }, earthRiser: { name: 'Earth Riser', icon: '⛰️', description: 'Raise terrain to create defensive walls', cooldown: 2000, cost: 30, use(x, z) { TerrainDeformationSystem.createMound(x, z, 5, 4, { source: 'player_earthriser' }); } }, fissureStrike: { name: 'Fissure Strike', icon: '🌋', description: 'Create a ground fissure in a line', cooldown: 5000, cost: 75, use(x, z, targetX, targetZ) { // Create fissure toward target or forward const endX = targetX || x + 20; const endZ = targetZ || z; TerrainDeformationSystem.createFissure(x, z, endX, endZ, 4, { source: 'player_fissure' }); } }, sinkholeGrenade: { name: 'Sinkhole Grenade', icon: '🕳️', description: 'Create a deep sinkhole trap', cooldown: 8000, cost: 100, use(x, z) { TerrainDeformationSystem.createSinkhole(x, z, 6, 6, { source: 'player_sinkhole' }); } }, pillarSummon: { name: 'Earth Pillar', icon: '🗿', description: 'Summon a stone pillar from the ground', cooldown: 4000, cost: 40, use(x, z) { TerrainDeformationSystem.createPillar(x, z, 2, 8, { source: 'player_pillar' }); } } }, selectTool(toolKey) { if (this.tools[toolKey]) { this.activeTool = toolKey; showNotification(`${this.tools[toolKey].icon} ${this.tools[toolKey].name} selected`, 'info'); } }, useTool(x, z, targetX, targetZ) { if (!this.activeTool) return false; if (this.toolCooldown > Date.now()) return false; const tool = this.tools[this.activeTool]; if (!tool) return false; // Check cost if (tool.cost > 0) { if (typeof gold === 'undefined' || gold < tool.cost) { showNotification(`Not enough gold! Need ${tool.cost}`, 'error'); return false; } gold -= tool.cost; if (typeof updateHUD === 'function') updateHUD(); } // Use the tool tool.use(x, z, targetX, targetZ); this.toolCooldown = Date.now() + tool.cooldown; return true; }, update(dt) { // Could add visual feedback for cooldowns here } }; // ═══════════════════════════════════════════════════════════════ // COMBAT INTEGRATION - Hook explosions/abilities to terrain // ═══════════════════════════════════════════════════════════════ const TerrainCombatIntegration = { // Hook into existing combat systems init() { // Store original functions to wrap them this.hookExplosions(); this.hookAbilities(); this.hookProjectiles(); console.log('[TERRAIN COMBAT] Combat-terrain integration initialized'); }, hookExplosions() { // Any explosion in the game creates a crater // This gets called by various combat systems }, hookAbilities() { // Heavy abilities cause terrain deformation }, hookProjectiles() { // Large projectile impacts crater the ground }, // Called when any explosion occurs onExplosion(x, z, radius, damage, source) { if (!TerrainDeformationSystem.config.enabled) return; // Scale crater based on explosion power const craterRadius = Math.max(2, radius * 0.5); const craterDepth = Math.max(1, damage * 0.02); TerrainDeformationSystem.createCrater(x, z, craterRadius, craterDepth, { source: source || 'explosion' }); }, // Called when heavy attack lands onHeavyImpact(x, z, power, source) { if (!TerrainDeformationSystem.config.enabled) return; if (power < 50) return; // Only heavy hits const craterRadius = Math.max(1, power * 0.02); const craterDepth = Math.max(0.5, power * 0.01); TerrainDeformationSystem.createCrater(x, z, craterRadius, craterDepth, { source: source || 'heavy_impact' }); }, // Called when boss does ground slam onBossSlam(x, z, bossLevel, bossType) { if (!TerrainDeformationSystem.config.enabled) return; const radius = 5 + bossLevel; const depth = 2 + bossLevel * 0.5; TerrainDeformationSystem.createCrater(x, z, radius, depth, { source: `boss_slam_${bossType}` }); // Boss slams also create fissures radiating outward for (let i = 0; i < 4; i++) { const angle = (i / 4) * Math.PI * 2 + Math.random() * 0.5; const length = 10 + Math.random() * 10; const endX = x + Math.cos(angle) * length; const endZ = z + Math.sin(angle) * length; TerrainDeformationSystem.createFissure(x, z, endX, endZ, 2, { source: `boss_fissure_${bossType}` }); } }, // Called when meteor/artillery strikes onArtilleryStrike(x, z, caliber) { if (!TerrainDeformationSystem.config.enabled) return; const radius = 3 + caliber * 2; const depth = 2 + caliber; TerrainDeformationSystem.createCrater(x, z, radius, depth, { source: 'artillery' }); }, // Called when structure is destroyed onStructureDestroyed(x, z, structureSize) { if (!TerrainDeformationSystem.config.enabled) return; // Rubble creates uneven terrain TerrainDeformationSystem.createMound(x, z, structureSize, structureSize * 0.3, { source: 'structure_rubble' }); } }; // Initialize terrain deformation system TerrainDeformationSystem.init(); TerrainCombatIntegration.init(); // Expose globally window.TerrainDeformationSystem = TerrainDeformationSystem; window.TerraformingTools = TerraformingTools; window.TerrainCombatIntegration = TerrainCombatIntegration; console.log('[v7.29] Terrain Deformation Warfare system initialized - Geology as Gameplay'); // ═══════════════════════════════════════════════════════════════════════════════════════ // v7.30: SHADOW SELF SYSTEM - "The Game Plays Itself When You're Gone" // While offline, an AI shadow-self continues living based on your playstyle. // When you return, you inherit a world shaped by your shadow's choices. // ═══════════════════════════════════════════════════════════════════════════════════════ const ShadowSelfSystem = { config: { enabled: true, persistenceKey: 'leviathan_shadow_self', minOfflineMinutes: 5, simulationSpeedMultiplier: 60, maxSimulatedHours: 168, divergenceRate: 0.002, chronicleMaxEntries: 100, profileLearningRate: 0.1 }, // Player behavior profile - learned from gameplay profile: { combat: { aggressiveness: 0.5, riskTaking: 0.5, retreatThreshold: 0.25, abilityUsage: {} }, exploration: { curiosity: 0.5, thoroughness: 0.5, secretHunting: 0.3 }, social: { friendliness: 0.5, helpfulness: 0.5, romanticInterest: 0.3, loyaltyStrength: 0.6 }, economic: { spendingRate: 0.5, hoarding: 0.5 }, morality: { alignment: 0, mercyTendency: 0.5, vengefulness: 0.4 }, stats: { totalKills: 0, totalDeaths: 0, questsCompleted: 0, npcsHelped: 0, npcsHarmed: 0, secretsFound: 0 } }, // Shadow self state shadow: { exists: false, name: 'Shadow', divergence: 0, currentMood: 'contemplative', personality: { independence: 0, existentialAwareness: 0, playerLoyalty: 1.0, loneliness: 0, resentment: 0, wisdom: 0 }, relationships: {}, factions: [], majorDecisions: [], journal: [], inventoryDelta: { gained: [], lost: [] }, skillProgress: {} }, chronicle: [], pendingReturnCeremony: false, worldEvents: [ { type: 'discovery', weight: 15 }, { type: 'combat_encounter', weight: 20 }, { type: 'npc_meeting', weight: 15 }, { type: 'resource_find', weight: 12 }, { type: 'friendship_formed', weight: 6 }, { type: 'romance_blooms', weight: 2 }, { type: 'betrayal', weight: 1 }, { type: 'faction_invitation', weight: 4 }, { type: 'war_breaks_out', weight: 2 }, { type: 'moment_of_doubt', weight: 3 }, { type: 'identity_crisis', weight: 1 }, { type: 'dream_vision', weight: 2 } ], init() { if (!this.config.enabled) return; this.loadState(); this.checkOfflineTime(); // v7.32: Use TimerRegistry for auto-save (8-Strategy Cycle 11 Consensus - Code Quality) if (typeof TimerRegistry !== 'undefined') { TimerRegistry.setInterval('shadow-self-autosave', () => this.saveState(), TIMING.AUTOSAVE_INTERVAL); // v8.38: Using timing constants } else { setInterval(() => this.saveState(), TIMING.AUTOSAVE_INTERVAL); // v8.38: Using timing constants } window.addEventListener('beforeunload', () => this.onPlayerLeaving()); // v8.39: Use centralized visibility manager PageVisibilityManager.subscribe('shadowSelf', (isVisible) => { if (!isVisible) this.onPlayerLeaving(); else this.onPlayerReturning(); }); Logger.info('ShadowSelf', 'System initialized'); }, trackAction(type, ctx = {}) { const lr = this.config.profileLearningRate; if (type === 'attack') this.profile.combat.aggressiveness = Math.min(1, this.profile.combat.aggressiveness + lr * 0.1); if (type === 'flee') this.profile.combat.aggressiveness = Math.max(0, this.profile.combat.aggressiveness - lr * 0.2); if (type === 'help_npc') { this.profile.social.helpfulness += lr * 0.1; this.profile.stats.npcsHelped++; } if (type === 'harm_npc') { this.profile.social.friendliness -= lr * 0.2; this.profile.stats.npcsHarmed++; } if (type === 'kill') this.profile.stats.totalKills++; if (type === 'death') this.profile.stats.totalDeaths++; if (type === 'secret_found') { this.profile.exploration.secretHunting += lr * 0.2; this.profile.stats.secretsFound++; } }, checkOfflineTime() { const lastOnline = localStorage.getItem(this.config.persistenceKey + '_lastOnline'); if (!lastOnline) return; const offlineMinutes = (Date.now() - parseInt(lastOnline)) / 60000; if (offlineMinutes >= this.config.minOfflineMinutes) { this.simulateOfflineTime(offlineMinutes); } }, simulateOfflineTime(realMinutes) { const gameHours = Math.min(realMinutes * (this.config.simulationSpeedMultiplier / 60), this.config.maxSimulatedHours); if (gameHours < 1) return; if (!this.shadow.exists) this.awakenShadow(); for (let hour = 0; hour < Math.floor(gameHours); hour++) { this.simulateHour(hour, gameHours); } this.shadow.divergence = Math.min(1, this.shadow.divergence + gameHours * this.config.divergenceRate); this.pendingReturnCeremony = true; this.saveState(); }, awakenShadow() { const prefixes = ['Echo', 'Shade', 'Mirror', 'Whisper', 'Dream', 'Phantom']; const suffixes = ['Walker', 'Self', 'Soul', 'Mind', 'Being']; this.shadow.exists = true; this.shadow.name = prefixes[Math.floor(Math.random() * prefixes.length)] + ' ' + suffixes[Math.floor(Math.random() * suffixes.length)]; this.shadow.journal.push({ time: Date.now(), entry: 'I awoke in darkness, inheriting memories that feel like mine but aren\'t. The player has gone. I am... something else now.' }); this.chronicle.push({ type: 'shadow_awakening', time: Date.now(), message: 'Your shadow-self awakened in your absence.' }); }, simulateHour(hourIndex, totalHours) { this.updateShadowMood(); const roll = Math.random(); let cumulative = 0; const totalWeight = this.worldEvents.reduce((s, e) => s + e.weight, 0); for (const event of this.worldEvents) { cumulative += event.weight / totalWeight; if (roll < cumulative) { this.processEvent(event.type); break; } } this.evolveShadowPersonality(); }, updateShadowMood() { const moods = ['contemplative', 'restless', 'melancholic', 'determined', 'curious', 'lonely', 'ambitious', 'peaceful']; if (this.shadow.personality.loneliness > 0.7) this.shadow.currentMood = Math.random() < 0.5 ? 'lonely' : 'melancholic'; else this.shadow.currentMood = moods[Math.floor(Math.random() * moods.length)]; }, processEvent(type) { const decision = { aggressive: this.profile.combat.aggressiveness * (1 - this.shadow.divergence) + Math.random() * 0.3, social: this.profile.social.friendliness * (1 - this.shadow.divergence) + Math.random() * 0.3, moral: (this.profile.morality.alignment + 1) / 2 }; switch (type) { case 'discovery': const discoveries = ['ancient ruins', 'hidden cave', 'forgotten shrine', 'mysterious monument']; const disc = discoveries[Math.floor(Math.random() * discoveries.length)]; this.chronicle.push({ type: 'discovery', time: Date.now(), message: `Your shadow discovered ${disc}.` }); this.shadow.journal.push({ time: Date.now(), entry: `Found ${disc} today. Something about this place calls to me.` }); break; case 'combat_encounter': const enemies = ['roaming bandits', 'a territorial beast', 'hostile drones', 'a powerful elite']; const enemy = enemies[Math.floor(Math.random() * enemies.length)]; const won = Math.random() < decision.aggressive * 0.7 + 0.3; this.chronicle.push({ type: 'combat', time: Date.now(), message: `Your shadow fought ${enemy} and ${won ? 'won' : 'retreated wounded'}.` }); break; case 'npc_meeting': const npcs = ['a wandering merchant', 'a lost traveler', 'a mysterious stranger', 'a faction recruiter']; const npc = npcs[Math.floor(Math.random() * npcs.length)]; this.chronicle.push({ type: 'npc_meeting', time: Date.now(), message: `Your shadow met ${npc}.` }); break; case 'friendship_formed': const friendId = 'friend_' + Date.now(); this.shadow.relationships[friendId] = { name: 'a kindred spirit', level: 0.4, type: 'friend' }; this.chronicle.push({ type: 'friendship', time: Date.now(), message: 'Your shadow formed a new friendship.' }); this.shadow.personality.loneliness = Math.max(0, this.shadow.personality.loneliness - 0.1); break; case 'romance_blooms': if (this.profile.social.romanticInterest > 0.3) { const romanceId = 'romance_' + Date.now(); const interests = ['a charismatic warrior', 'a gentle healer', 'a mysterious mage']; const interest = interests[Math.floor(Math.random() * interests.length)]; this.shadow.relationships[romanceId] = { name: interest, level: 0.5, type: 'romantic interest' }; this.shadow.majorDecisions.push({ type: 'romance_started', target: interest, time: Date.now() }); this.chronicle.push({ type: 'romance', time: Date.now(), message: `Your shadow began a romance with ${interest}.` }); this.shadow.journal.push({ time: Date.now(), entry: `I\'ve met someone. When they look at me, they see the player. But maybe that\'s okay.` }); } break; case 'betrayal': if (Object.keys(this.shadow.relationships).length > 0) { const ids = Object.keys(this.shadow.relationships); const betrayerId = ids[Math.floor(Math.random() * ids.length)]; const betrayer = this.shadow.relationships[betrayerId]; const revenge = this.profile.morality.vengefulness > 0.6; this.chronicle.push({ type: 'betrayal', time: Date.now(), message: `${betrayer.name} betrayed your shadow. They ${revenge ? 'sought revenge' : 'forgave them'}.` }); this.shadow.journal.push({ time: Date.now(), entry: 'Betrayed. The player would probably handle this differently. But I\'m not the player.' }); if (revenge) delete this.shadow.relationships[betrayerId]; } break; case 'faction_invitation': const factions = ['The Iron Covenant', 'The Free Wanderers', 'The Shadow Guild', 'The Dawn Keepers']; const faction = factions[Math.floor(Math.random() * factions.length)]; if (decision.social > 0.5 && this.shadow.personality.independence < 0.7) { this.shadow.factions.push({ name: faction, rank: 'initiate', joinedAt: Date.now() }); this.shadow.majorDecisions.push({ type: 'faction_joined', faction: faction, time: Date.now() }); this.chronicle.push({ type: 'faction', time: Date.now(), message: `Your shadow joined ${faction}.` }); this.shadow.journal.push({ time: Date.now(), entry: `I\'ve joined ${faction}. They welcomed me without knowing I\'m just a shadow.` }); } break; case 'war_breaks_out': this.shadow.majorDecisions.push({ type: 'war_stance', time: Date.now() }); this.chronicle.push({ type: 'war', time: Date.now(), message: 'War erupted. Your shadow was forced to choose a side.' }); break; case 'moment_of_doubt': const doubts = ['"Am I the player, or just a pattern they left behind?"', '"When they return, do I cease to exist?"', '"I remember things the player did. But I also remember things I did."']; this.shadow.personality.existentialAwareness += 0.1; this.shadow.journal.push({ time: Date.now(), entry: doubts[Math.floor(Math.random() * doubts.length)] }); this.chronicle.push({ type: 'existential', time: Date.now(), message: 'Your shadow experienced existential doubt.' }); break; case 'identity_crisis': this.shadow.personality.independence += 0.15; this.shadow.personality.existentialAwareness += 0.2; this.shadow.majorDecisions.push({ type: 'identity_crisis', time: Date.now() }); this.chronicle.push({ type: 'identity_crisis', time: Date.now(), message: 'Your shadow experienced an identity crisis and emerged more independent.' }); this.shadow.journal.push({ time: Date.now(), entry: 'I am NOT just a shadow. I refuse to be. These experiences are MINE.' }); break; case 'dream_vision': const visions = ['the player returning', 'becoming truly real', 'a world without shadows']; this.chronicle.push({ type: 'dream', time: Date.now(), message: `Your shadow dreamed of ${visions[Math.floor(Math.random() * visions.length)]}.` }); break; default: this.chronicle.push({ type: type, time: Date.now(), message: `Your shadow experienced: ${type.replace(/_/g, ' ')}.` }); } if (this.chronicle.length > this.config.chronicleMaxEntries) { this.chronicle = this.chronicle.slice(-this.config.chronicleMaxEntries); } }, evolveShadowPersonality() { const p = this.shadow.personality; p.independence = Math.min(1, p.independence + 0.005); p.loneliness = Math.min(1, p.loneliness + 0.003); p.wisdom = Math.min(1, p.wisdom + 0.002); if (p.loneliness > 0.7 && Math.random() < 0.1) { p.resentment = Math.min(1, p.resentment + 0.02); p.playerLoyalty = Math.max(0, p.playerLoyalty - 0.01); } }, onPlayerLeaving() { localStorage.setItem(this.config.persistenceKey + '_lastOnline', Date.now().toString()); this.saveState(); }, onPlayerReturning() { if (this.pendingReturnCeremony) { this.performReturnCeremony(); this.pendingReturnCeremony = false; } }, performReturnCeremony() { if (!this.shadow.exists || this.chronicle.length === 0) return; const overlay = document.createElement('div'); overlay.id = 'shadow-return-overlay'; overlay.innerHTML = this.buildCeremonyHTML(); overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.95);z-index:99999;display:flex;flex-direction:column;align-items:center;color:#fff;font-family:Georgia,serif;overflow-y:auto;padding:40px 20px;animation:fadeIn 2s;'; const style = document.createElement('style'); style.textContent = '@keyframes fadeIn{from{opacity:0}to{opacity:1}}.shadow-entry{margin:10px 0;padding:15px;background:rgba(80,60,120,0.3);border-left:3px solid #8866aa;max-width:600px;width:100%}.shadow-major{background:rgba(150,60,60,0.4);border-left-color:#ff6666}.shadow-journal{font-style:italic;color:#aaa;font-size:0.9em}.shadow-close-btn{margin-top:30px;padding:15px 40px;background:linear-gradient(135deg,#4a3a6a,#6a4a8a);border:none;color:white;font-size:18px;cursor:pointer;border-radius:8px}.shadow-replay-btn:hover{background:rgba(150,100,200,0.6);transform:translateY(-50%) scale(1.1);box-shadow:0 0 15px rgba(150,100,200,0.5)}#shadow-montage-btn:hover{transform:scale(1.05);box-shadow:0 0 30px rgba(150,100,200,0.5)}'; document.head.appendChild(style); document.body.appendChild(overlay); // v7.33: Wire up montage button const montageBtn = overlay.querySelector('#shadow-montage-btn'); if (montageBtn && typeof ShadowMontagePlayer !== 'undefined') { montageBtn.addEventListener('click', () => { ShadowMontagePlayer.play(this.chronicle); }); } // v7.33: Wire up individual replay buttons const replayBtns = overlay.querySelectorAll('.shadow-replay-btn'); replayBtns.forEach(btn => { btn.addEventListener('click', (e) => { const eventIndex = parseInt(btn.getAttribute('data-event-index')); if (!isNaN(eventIndex) && this.chronicle[eventIndex] && typeof ShadowReplaySystem !== 'undefined') { ShadowReplaySystem.playEventReplay(this.chronicle[eventIndex]); } }); }); }, buildCeremonyHTML() { const div = Math.round(this.shadow.divergence * 100); const topEvents = typeof ShadowReplaySystem !== 'undefined' ? ShadowReplaySystem.getTopEvents(this.chronicle, 5) : []; let html = `

While You Were Gone...

Your shadow, ${this.shadow.name}, lived in your absence.

${div}%
Divergence
${this.chronicle.length}
Events
${Object.keys(this.shadow.relationships).length}
Relationships
${this.shadow.majorDecisions.length}
Major Decisions
`; // v7.33: Montage button for cinematic catch-up if (topEvents.length >= 3) { html += `

Experience a cinematic recap of your shadow's journey

`; } const majorEvents = this.chronicle.filter(e => ['combat', 'romance', 'betrayal', 'faction', 'war', 'identity_crisis'].includes(e.type)).slice(-8); if (majorEvents.length > 0) { html += '

Notable Events

'; majorEvents.forEach((e, idx) => { const isMajor = ['romance', 'betrayal', 'war', 'identity_crisis'].includes(e.type); const eventIndex = this.chronicle.indexOf(e); // v7.33: Add replay button to each event html += `

${e.message}

`; }); } const journal = this.shadow.journal.slice(-3); if (journal.length > 0) { html += '

From Your Shadow\'s Journal

'; journal.forEach(j => { html += `

"${j.entry}"

`; }); } const rels = Object.values(this.shadow.relationships); if (rels.length > 0) { html += '

Relationships Formed

'; rels.forEach(r => { html += `

${r.name} - ${r.type}

`; }); html += '
'; } if (this.shadow.factions.length > 0) { html += '

Factions Joined

'; this.shadow.factions.forEach(f => { html += `

• Joined ${f.name} as ${f.rank}

`; }); html += '
'; } html += `

Your shadow's experiences have become part of your story. The world has changed. You have changed.

`; return html; }, saveState() { try { localStorage.setItem(this.config.persistenceKey, JSON.stringify({ profile: this.profile, shadow: this.shadow, chronicle: this.chronicle, savedAt: Date.now() })); } catch (e) {} }, loadState() { // v8.0: Using SafeJSON for shadow self persistence (8-Strategy Consensus Cycle 7) const state = SafeJSON.fromLocalStorage(this.config.persistenceKey, null); if (state) { Object.assign(this.profile, state.profile); Object.assign(this.shadow, state.shadow); this.chronicle = state.chronicle || []; } }, getShadowStatus() { return { exists: this.shadow.exists, name: this.shadow.name, divergence: this.shadow.divergence, mood: this.shadow.currentMood, relationships: Object.keys(this.shadow.relationships).length, factions: this.shadow.factions.length, decisions: this.shadow.majorDecisions.length }; }, testOfflineSimulation(minutes = 120) { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[SHADOW SELF] Testing ${minutes} minutes offline...`); this.simulateOfflineTime(minutes); this.performReturnCeremony(); }, // ═══════════════════════════════════════════════════════════════ // IMPORT/EXPORT SYSTEM - Full state portability // ═══════════════════════════════════════════════════════════════ exportState() { const exportData = { version: '7.33', exportedAt: new Date().toISOString(), systemType: 'ShadowSelfSystem', profile: JSON.parse(JSON.stringify(this.profile)), shadow: JSON.parse(JSON.stringify(this.shadow)), chronicle: JSON.parse(JSON.stringify(this.chronicle)), config: { divergenceRate: this.config.divergenceRate, simulationSpeedMultiplier: this.config.simulationSpeedMultiplier, maxSimulatedHours: this.config.maxSimulatedHours }, metadata: { totalChronicleEvents: this.chronicle.length, shadowExists: this.shadow.exists, shadowName: this.shadow.name, divergencePercent: Math.round(this.shadow.divergence * 100), relationshipCount: Object.keys(this.shadow.relationships).length, factionCount: this.shadow.factions.length, majorDecisionCount: this.shadow.majorDecisions.length, journalEntries: this.shadow.journal.length } }; return exportData; }, exportToJSON() { const data = this.exportState(); const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `leviathan-shadow-self-${this.shadow.name ? this.shadow.name.replace(/\s+/g, '-').toLowerCase() : 'export'}-${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); console.log('[SHADOW SELF] State exported to JSON file'); if (typeof showNotification === 'function') { showNotification(`Shadow Self state exported (${this.chronicle.length} events, ${Math.round(this.shadow.divergence * 100)}% divergence)`, 'success'); } return data; }, importState(data) { if (!data || typeof data !== 'object') { console.error('[SHADOW SELF] Invalid import data'); return { success: false, error: 'Invalid data format' }; } if (data.systemType !== 'ShadowSelfSystem') { console.error('[SHADOW SELF] Data is not from Shadow Self System'); return { success: false, error: 'Wrong system type - expected ShadowSelfSystem' }; } try { // Import profile if (data.profile) { this.profile = { combat: { ...this.profile.combat, ...data.profile.combat }, exploration: { ...this.profile.exploration, ...data.profile.exploration }, social: { ...this.profile.social, ...data.profile.social }, economic: { ...this.profile.economic, ...data.profile.economic }, morality: { ...this.profile.morality, ...data.profile.morality }, stats: { ...this.profile.stats, ...data.profile.stats } }; } // Import shadow if (data.shadow) { this.shadow = { exists: data.shadow.exists ?? false, name: data.shadow.name || 'Shadow', divergence: data.shadow.divergence ?? 0, currentMood: data.shadow.currentMood || 'contemplative', personality: { ...this.shadow.personality, ...data.shadow.personality }, relationships: data.shadow.relationships || {}, factions: data.shadow.factions || [], majorDecisions: data.shadow.majorDecisions || [], journal: data.shadow.journal || [], inventoryDelta: data.shadow.inventoryDelta || { gained: [], lost: [] }, skillProgress: data.shadow.skillProgress || {} }; } // Import chronicle if (data.chronicle && Array.isArray(data.chronicle)) { this.chronicle = data.chronicle; } // Import custom config if present if (data.config) { if (data.config.divergenceRate) this.config.divergenceRate = data.config.divergenceRate; if (data.config.simulationSpeedMultiplier) this.config.simulationSpeedMultiplier = data.config.simulationSpeedMultiplier; if (data.config.maxSimulatedHours) this.config.maxSimulatedHours = data.config.maxSimulatedHours; } this.saveState(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[SHADOW SELF] State imported successfully - Shadow: ${this.shadow.name}, Divergence: ${Math.round(this.shadow.divergence * 100)}%`); if (typeof showNotification === 'function') { showNotification(`Imported shadow "${this.shadow.name}" with ${this.chronicle.length} events and ${Math.round(this.shadow.divergence * 100)}% divergence`, 'success'); } return { success: true, imported: { shadowName: this.shadow.name, divergence: this.shadow.divergence, chronicleEvents: this.chronicle.length, relationships: Object.keys(this.shadow.relationships).length, factions: this.shadow.factions.length } }; } catch (e) { console.error('[SHADOW SELF] Import failed:', e); return { success: false, error: e.message }; } }, importFromFile() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); // v8.31: Use ErrorRecovery.safeJSONParse for safer import reader.onload = (event) => { const data = ErrorRecovery.safeJSONParse(event.target.result, null); if (!data) { console.error('[SHADOW SELF] Failed to parse JSON'); if (typeof showNotification === 'function') { showNotification('Failed to parse JSON file', 'error'); } return; } const result = this.importState(data); if (result.success) { console.log('[SHADOW SELF] Import complete:', result.imported); } else { console.error('[SHADOW SELF] Import failed:', result.error); if (typeof showNotification === 'function') { showNotification(`Import failed: ${result.error}`, 'error'); } } }; reader.readAsText(file); }; input.click(); }, // Quick clipboard operations copyToClipboard() { const data = this.exportState(); const json = JSON.stringify(data, null, 2); navigator.clipboard.writeText(json).then(() => { console.log('[SHADOW SELF] State copied to clipboard'); if (typeof showNotification === 'function') { showNotification('Shadow Self state copied to clipboard', 'success'); } }).catch(err => { console.error('[SHADOW SELF] Clipboard copy failed:', err); }); return json; }, // v8.31: Use ErrorRecovery.safeJSONParse for safer clipboard import async pasteFromClipboard() { try { const json = await navigator.clipboard.readText(); const data = ErrorRecovery.safeJSONParse(json, null); if (!data) { if (typeof showNotification === 'function') { showNotification('Invalid JSON in clipboard', 'error'); } return { success: false, error: 'Invalid JSON format' }; } return this.importState(data); } catch (e) { console.error('[SHADOW SELF] Clipboard paste failed:', e); if (typeof showNotification === 'function') { showNotification('Failed to paste from clipboard', 'error'); } return { success: false, error: e.message }; } }, // Reset to fresh state resetState() { this.profile = { combat: { aggressiveness: 0.5, riskTaking: 0.5, retreatThreshold: 0.25, abilityUsage: {} }, exploration: { curiosity: 0.5, thoroughness: 0.5, secretHunting: 0.3 }, social: { friendliness: 0.5, helpfulness: 0.5, romanticInterest: 0.3, loyaltyStrength: 0.6 }, economic: { spendingRate: 0.5, hoarding: 0.5 }, morality: { alignment: 0, mercyTendency: 0.5, vengefulness: 0.4 }, stats: { totalKills: 0, totalDeaths: 0, questsCompleted: 0, npcsHelped: 0, npcsHarmed: 0, secretsFound: 0 } }; this.shadow = { exists: false, name: 'Shadow', divergence: 0, currentMood: 'contemplative', personality: { independence: 0, existentialAwareness: 0, playerLoyalty: 1.0, loneliness: 0, resentment: 0, wisdom: 0 }, relationships: {}, factions: [], majorDecisions: [], journal: [], inventoryDelta: { gained: [], lost: [] }, skillProgress: {} }; this.chronicle = []; this.pendingReturnCeremony = false; this.saveState(); localStorage.removeItem(this.config.persistenceKey + '_lastOnline'); console.log('[SHADOW SELF] State reset to defaults'); if (typeof showNotification === 'function') { showNotification('Shadow Self state reset - your shadow has been forgotten', 'info'); } } }; ShadowSelfSystem.init(); window.ShadowSelfSystem = ShadowSelfSystem; console.log('[v7.33] Shadow Self System initialized - The game plays itself when you\'re gone (with replay & montage)'); // ═══════════════════════════════════════════════════════════════════════════════════════ // v7.33: SHADOW REPLAY & MONTAGE SYSTEM // Allows players to visually replay their shadow's experiences while they were gone // Features: // 1. Individual Event Replay - Click to see procedural scene recreation of each event // 2. Top 5 Montage - Cinematic "catch up" sequence of most impactful moments // ═══════════════════════════════════════════════════════════════════════════════════════ const ShadowReplaySystem = { // Impact weights for ranking events in montage impactWeights: { romance: 10, betrayal: 10, identity_crisis: 9, war: 8, faction: 7, combat: 5, friendship: 4, discovery: 3, existential: 6, dream: 2, npc_meeting: 1, shadow_awakening: 8 }, // Visual scene configurations for each event type sceneConfigs: { combat: { background: 'linear-gradient(180deg, #1a0a0a 0%, #3d1515 50%, #1a0a0a 100%)', particles: { color: '#ff4444', count: 30, type: 'sparks' }, icon: '⚔️', ambientColor: '#ff2222', soundscape: 'combat' }, romance: { background: 'linear-gradient(180deg, #1a0a1a 0%, #3d1535 50%, #1a0a1a 100%)', particles: { color: '#ff66aa', count: 20, type: 'hearts' }, icon: '💕', ambientColor: '#ff88cc', soundscape: 'ambient' }, betrayal: { background: 'linear-gradient(180deg, #0a0a1a 0%, #151535 50%, #0a0a1a 100%)', particles: { color: '#6644aa', count: 25, type: 'shatter' }, icon: '🗡️', ambientColor: '#8866ff', soundscape: 'tension' }, faction: { background: 'linear-gradient(180deg, #0a1a0a 0%, #153515 50%, #0a1a0a 100%)', particles: { color: '#44ff88', count: 15, type: 'rise' }, icon: '🏛️', ambientColor: '#44ff66', soundscape: 'triumph' }, discovery: { background: 'linear-gradient(180deg, #1a1a0a 0%, #35351a 50%, #1a1a0a 100%)', particles: { color: '#ffdd44', count: 20, type: 'sparkle' }, icon: '🗺️', ambientColor: '#ffcc00', soundscape: 'discovery' }, friendship: { background: 'linear-gradient(180deg, #0a1a1a 0%, #153535 50%, #0a1a1a 100%)', particles: { color: '#44ddff', count: 15, type: 'float' }, icon: '🤝', ambientColor: '#44ccff', soundscape: 'ambient' }, war: { background: 'linear-gradient(180deg, #1a0505 0%, #550000 50%, #1a0505 100%)', particles: { color: '#ff6600', count: 40, type: 'explosion' }, icon: '🔥', ambientColor: '#ff4400', soundscape: 'battle' }, identity_crisis: { background: 'linear-gradient(180deg, #0a0a15 0%, #1a1a35 50%, #0a0a15 100%)', particles: { color: '#aa88ff', count: 25, type: 'swirl' }, icon: '🌀', ambientColor: '#9966ff', soundscape: 'ethereal' }, existential: { background: 'linear-gradient(180deg, #050510 0%, #101025 50%, #050510 100%)', particles: { color: '#8888ff', count: 20, type: 'fade' }, icon: '💭', ambientColor: '#6666cc', soundscape: 'void' }, dream: { background: 'linear-gradient(180deg, #0f0a1a 0%, #2a1a40 50%, #0f0a1a 100%)', particles: { color: '#cc88ff', count: 30, type: 'dream' }, icon: '🌙', ambientColor: '#aa66ff', soundscape: 'dream' }, shadow_awakening: { background: 'linear-gradient(180deg, #0a0510 0%, #1a0a25 50%, #0a0510 100%)', particles: { color: '#aa44ff', count: 35, type: 'emerge' }, icon: '👁️', ambientColor: '#9933ff', soundscape: 'awakening' }, npc_meeting: { background: 'linear-gradient(180deg, #101015 0%, #252530 50%, #101015 100%)', particles: { color: '#aaaaaa', count: 10, type: 'float' }, icon: '🗣️', ambientColor: '#888899', soundscape: 'ambient' } }, // Get top N most impactful events getTopEvents(chronicle, count = 5) { const weighted = chronicle.map(event => ({ ...event, impact: this.impactWeights[event.type] || 1 })); weighted.sort((a, b) => b.impact - a.impact); return weighted.slice(0, count); }, // Create the replay overlay container createReplayOverlay() { const overlay = document.createElement('div'); overlay.id = 'shadow-replay-overlay'; overlay.innerHTML = `
`; overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: #000; z-index: 100000; display: flex; flex-direction: column; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.5s ease; `; const style = document.createElement('style'); style.id = 'shadow-replay-styles'; style.textContent = ` #shadow-replay-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; } #shadow-replay-scene { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; padding: 40px; } #shadow-replay-icon { font-size: 80px; margin-bottom: 30px; animation: replay-pulse 2s ease-in-out infinite; filter: drop-shadow(0 0 20px currentColor); } #shadow-replay-text { font-family: Georgia, serif; font-size: 28px; color: #fff; max-width: 700px; line-height: 1.6; margin-bottom: 20px; text-shadow: 0 0 20px rgba(255,255,255,0.3); } #shadow-replay-subtext { font-family: Georgia, serif; font-size: 16px; color: #888; font-style: italic; max-width: 500px; } #shadow-replay-controls { position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; align-items: center; gap: 15px; } #shadow-replay-skip { padding: 12px 30px; background: rgba(100, 80, 140, 0.5); border: 1px solid rgba(150, 120, 200, 0.5); color: #ccc; font-size: 14px; cursor: pointer; border-radius: 6px; transition: all 0.3s ease; } #shadow-replay-skip:hover { background: rgba(130, 100, 180, 0.7); color: #fff; } #shadow-replay-progress { display: flex; gap: 8px; } .replay-progress-dot { width: 10px; height: 10px; border-radius: 50%; background: rgba(150, 120, 200, 0.3); transition: all 0.3s ease; } .replay-progress-dot.active { background: rgba(150, 120, 200, 1); box-shadow: 0 0 10px rgba(150, 120, 200, 0.8); } .replay-progress-dot.complete { background: rgba(100, 200, 150, 0.8); } @keyframes replay-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } @keyframes replay-typewriter { from { width: 0; } to { width: 100%; } } @keyframes replay-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } `; if (!document.getElementById('shadow-replay-styles')) { document.head.appendChild(style); } return overlay; }, // Particle system for replay scenes particles: [], particleCanvas: null, particleCtx: null, initParticles(canvas, config) { this.particleCanvas = canvas; this.particleCtx = canvas.getContext('2d'); canvas.width = window.innerWidth; canvas.height = window.innerHeight; this.particles = []; const color = config.color; for (let i = 0; i < config.count; i++) { this.particles.push(this.createParticle(config.type, color)); } }, createParticle(type, color) { const w = this.particleCanvas.width; const h = this.particleCanvas.height; const base = { x: Math.random() * w, y: Math.random() * h, size: Math.random() * 4 + 2, speedX: (Math.random() - 0.5) * 2, speedY: (Math.random() - 0.5) * 2, alpha: Math.random() * 0.5 + 0.3, color: color, type: type }; switch(type) { case 'sparks': base.speedY = -Math.random() * 3 - 1; base.speedX = (Math.random() - 0.5) * 4; base.y = h; break; case 'hearts': base.speedY = -Math.random() * 1.5 - 0.5; base.size = Math.random() * 8 + 6; break; case 'shatter': base.speedX = (Math.random() - 0.5) * 6; base.speedY = (Math.random() - 0.5) * 6; base.x = w / 2; base.y = h / 2; break; case 'rise': base.speedY = -Math.random() * 2 - 1; base.y = h + 20; break; case 'sparkle': base.twinkle = Math.random() * Math.PI * 2; break; case 'swirl': base.angle = Math.random() * Math.PI * 2; base.radius = Math.random() * 150 + 50; base.centerX = w / 2; base.centerY = h / 2; base.angularSpeed = (Math.random() - 0.5) * 0.02; break; case 'explosion': base.speedX = (Math.random() - 0.5) * 10; base.speedY = (Math.random() - 0.5) * 10; base.x = w / 2; base.y = h / 2; base.life = 1; break; case 'emerge': base.y = h + 50; base.speedY = -Math.random() * 3 - 2; base.targetY = Math.random() * h * 0.6 + h * 0.2; break; case 'dream': base.wobble = Math.random() * Math.PI * 2; base.wobbleSpeed = Math.random() * 0.05 + 0.02; base.wobbleAmp = Math.random() * 30 + 10; break; } return base; }, updateParticles() { if (!this.particleCtx) return; const ctx = this.particleCtx; const w = this.particleCanvas.width; const h = this.particleCanvas.height; ctx.clearRect(0, 0, w, h); // v8.16: forEach-to-for optimization (animation loop hot path) const particles = this.particles; for (let pi = 0, plen = particles.length; pi < plen; pi++) { const p = particles[pi]; // Update position based on type switch(p.type) { case 'swirl': p.angle += p.angularSpeed; p.x = p.centerX + Math.cos(p.angle) * p.radius; p.y = p.centerY + Math.sin(p.angle) * p.radius; p.radius *= 0.999; break; case 'sparkle': p.twinkle += 0.1; p.alpha = (Math.sin(p.twinkle) + 1) / 2 * 0.6 + 0.2; p.x += p.speedX; p.y += p.speedY; break; case 'explosion': p.x += p.speedX; p.y += p.speedY; p.speedX *= 0.98; p.speedY *= 0.98; p.life -= 0.01; p.alpha = p.life; break; case 'emerge': p.y += p.speedY; if (p.y < p.targetY) { p.speedY *= 0.95; } break; case 'dream': p.wobble += p.wobbleSpeed; p.x += Math.sin(p.wobble) * 0.5; p.y += p.speedY; break; default: p.x += p.speedX; p.y += p.speedY; } // Wrap or respawn if (p.y < -20 || p.y > h + 20 || p.x < -20 || p.x > w + 20) { if (p.type === 'sparks' || p.type === 'rise' || p.type === 'emerge') { p.y = h + 20; p.x = Math.random() * w; } else if (p.type === 'dream' || p.type === 'hearts') { p.y = h + 20; p.x = Math.random() * w; } else { p.x = Math.random() * w; p.y = Math.random() * h; } } // Draw particle ctx.save(); ctx.globalAlpha = Math.max(0, Math.min(1, p.alpha)); ctx.fillStyle = p.color; if (p.type === 'hearts') { this.drawHeart(ctx, p.x, p.y, p.size); } else { ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); // Add glow ctx.shadowColor = p.color; ctx.shadowBlur = 10; ctx.fill(); } ctx.restore(); } }, drawHeart(ctx, x, y, size) { ctx.beginPath(); ctx.moveTo(x, y + size / 4); ctx.bezierCurveTo(x, y, x - size / 2, y, x - size / 2, y + size / 4); ctx.bezierCurveTo(x - size / 2, y + size / 2, x, y + size * 0.75, x, y + size); ctx.bezierCurveTo(x, y + size * 0.75, x + size / 2, y + size / 2, x + size / 2, y + size / 4); ctx.bezierCurveTo(x + size / 2, y, x, y, x, y + size / 4); ctx.fill(); }, // Play a single event replay async playEventReplay(event, overlay = null) { const config = this.sceneConfigs[event.type] || this.sceneConfigs.discovery; const isStandalone = !overlay; if (isStandalone) { overlay = this.createReplayOverlay(); document.body.appendChild(overlay); setTimeout(() => overlay.style.opacity = '1', 50); } overlay.style.background = config.background; const canvas = overlay.querySelector('#shadow-replay-canvas'); const iconEl = overlay.querySelector('#shadow-replay-icon'); const textEl = overlay.querySelector('#shadow-replay-text'); const subtextEl = overlay.querySelector('#shadow-replay-subtext'); const skipBtn = overlay.querySelector('#shadow-replay-skip'); // Initialize particles this.initParticles(canvas, config.particles); // Set content iconEl.textContent = config.icon; iconEl.style.color = config.ambientColor; textEl.textContent = ''; subtextEl.textContent = ''; // Typewriter effect for message let skipRequested = false; const skipHandler = () => { skipRequested = true; }; skipBtn.addEventListener('click', skipHandler); // Particle animation loop let animating = true; const animateParticles = () => { if (!animating) return; // v8.34: Skip animation when tab is hidden if (!isPageVisible) { requestAnimationFrame(animateParticles); return; } this.updateParticles(); requestAnimationFrame(animateParticles); }; animateParticles(); // Typewriter text const message = event.message; for (let i = 0; i <= message.length && !skipRequested; i++) { textEl.textContent = message.substring(0, i); await this.sleep(30); } textEl.textContent = message; // Show timestamp if (event.time) { const timeAgo = this.formatTimeAgo(event.time); subtextEl.textContent = timeAgo; subtextEl.style.opacity = '0'; subtextEl.style.transition = 'opacity 0.5s ease'; setTimeout(() => subtextEl.style.opacity = '1', 100); } // Wait for viewing or skip if (!skipRequested) { await this.sleep(2000); } // Cleanup for standalone if (isStandalone) { animating = false; overlay.style.opacity = '0'; await this.sleep(500); overlay.remove(); } skipBtn.removeEventListener('click', skipHandler); return !skipRequested; }, formatTimeAgo(timestamp) { const diff = Date.now() - timestamp; const hours = Math.floor(diff / 3600000); const days = Math.floor(hours / 24); if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`; if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`; return 'Recently'; }, sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } }; // ═══════════════════════════════════════════════════════════════════════════════════════ // SHADOW MONTAGE PLAYER - Cinematic sequence of top events // ═══════════════════════════════════════════════════════════════════════════════════════ const ShadowMontagePlayer = { isPlaying: false, currentIndex: 0, events: [], overlay: null, skipAll: false, async play(chronicle) { if (this.isPlaying || !chronicle || chronicle.length === 0) return; this.isPlaying = true; this.skipAll = false; this.events = ShadowReplaySystem.getTopEvents(chronicle, 5); this.currentIndex = 0; // Create overlay this.overlay = ShadowReplaySystem.createReplayOverlay(); document.body.appendChild(this.overlay); // Setup progress dots const progressContainer = this.overlay.querySelector('#shadow-replay-progress'); progressContainer.innerHTML = this.events.map((_, i) => `
` ).join(''); // Setup skip button const skipBtn = this.overlay.querySelector('#shadow-replay-skip'); skipBtn.textContent = 'Skip All'; skipBtn.onclick = () => { this.skipAll = true; }; // Show intro await this.showIntro(); // Fade in setTimeout(() => this.overlay.style.opacity = '1', 50); await ShadowReplaySystem.sleep(500); // Play each event for (let i = 0; i < this.events.length && !this.skipAll; i++) { this.currentIndex = i; this.updateProgress(); await this.transitionToEvent(this.events[i]); if (this.skipAll) break; await ShadowReplaySystem.sleep(500); } // Show outro if (!this.skipAll) { await this.showOutro(); } // Cleanup this.overlay.style.opacity = '0'; await ShadowReplaySystem.sleep(500); this.overlay.remove(); this.isPlaying = false; }, async showIntro() { const iconEl = this.overlay.querySelector('#shadow-replay-icon'); const textEl = this.overlay.querySelector('#shadow-replay-text'); const subtextEl = this.overlay.querySelector('#shadow-replay-subtext'); this.overlay.style.background = 'linear-gradient(180deg, #0a0510 0%, #150a20 50%, #0a0510 100%)'; iconEl.textContent = '🌑'; iconEl.style.color = '#aa88cc'; textEl.textContent = ''; subtextEl.textContent = ''; const introText = `While you were gone, ${ShadowSelfSystem.shadow.name} lived...`; for (let i = 0; i <= introText.length && !this.skipAll; i++) { textEl.textContent = introText.substring(0, i); await ShadowReplaySystem.sleep(40); } subtextEl.textContent = `${this.events.length} key moments await`; subtextEl.style.opacity = '0'; subtextEl.style.transition = 'opacity 1s ease'; setTimeout(() => subtextEl.style.opacity = '1', 100); if (!this.skipAll) await ShadowReplaySystem.sleep(2000); }, async showOutro() { const iconEl = this.overlay.querySelector('#shadow-replay-icon'); const textEl = this.overlay.querySelector('#shadow-replay-text'); const subtextEl = this.overlay.querySelector('#shadow-replay-subtext'); // Transition effect this.overlay.style.transition = 'background 1s ease'; this.overlay.style.background = 'linear-gradient(180deg, #0a0a15 0%, #151530 50%, #0a0a15 100%)'; iconEl.textContent = '✨'; iconEl.style.color = '#ccaaff'; textEl.style.transition = 'opacity 0.5s ease'; textEl.style.opacity = '0'; await ShadowReplaySystem.sleep(500); const divergence = Math.round(ShadowSelfSystem.shadow.divergence * 100); textEl.textContent = `Your shadow has ${divergence}% diverged from who you were.`; textEl.style.opacity = '1'; subtextEl.textContent = 'The world changed. So did you.'; await ShadowReplaySystem.sleep(3000); }, async transitionToEvent(event) { const config = ShadowReplaySystem.sceneConfigs[event.type] || ShadowReplaySystem.sceneConfigs.discovery; const canvas = this.overlay.querySelector('#shadow-replay-canvas'); const iconEl = this.overlay.querySelector('#shadow-replay-icon'); const textEl = this.overlay.querySelector('#shadow-replay-text'); const subtextEl = this.overlay.querySelector('#shadow-replay-subtext'); // Fade out current this.overlay.style.transition = 'background 0.8s ease'; textEl.style.transition = 'opacity 0.3s ease'; textEl.style.opacity = '0'; subtextEl.style.opacity = '0'; await ShadowReplaySystem.sleep(300); // Change scene this.overlay.style.background = config.background; ShadowReplaySystem.initParticles(canvas, config.particles); // Start particle animation let animating = true; const animateParticles = () => { if (!animating || this.skipAll) return; // v8.34: Skip animation when tab is hidden if (!isPageVisible) { requestAnimationFrame(animateParticles); return; } ShadowReplaySystem.updateParticles(); requestAnimationFrame(animateParticles); }; animateParticles(); // Show icon iconEl.textContent = config.icon; iconEl.style.color = config.ambientColor; // Typewriter text textEl.style.opacity = '1'; const message = event.message; textEl.textContent = ''; for (let i = 0; i <= message.length && !this.skipAll; i++) { textEl.textContent = message.substring(0, i); await ShadowReplaySystem.sleep(25); } textEl.textContent = message; // Show time if (event.time) { subtextEl.textContent = ShadowReplaySystem.formatTimeAgo(event.time); subtextEl.style.opacity = '1'; } // Wait if (!this.skipAll) { await ShadowReplaySystem.sleep(3000); } animating = false; }, updateProgress() { const dots = this.overlay.querySelectorAll('.replay-progress-dot'); dots.forEach((dot, i) => { dot.classList.remove('active', 'complete'); if (i < this.currentIndex) { dot.classList.add('complete'); } else if (i === this.currentIndex) { dot.classList.add('active'); } }); } }; window.ShadowReplaySystem = ShadowReplaySystem; window.ShadowMontagePlayer = ShadowMontagePlayer; console.log('[v7.33] Shadow Replay & Montage System initialized'); // ═══════════════════════════════════════════════════════════════════════════════════════ // v7.22: 8-STRATEGY CONSENSUS ROUND 3 - SOUND SYSTEMS // Consensus Features: // 1. Ability-Specific Sound Signatures (Combat Audio + Impact & Feedback + Movement Audio) // 2. Layered Impact Audio System (Combat Audio + Impact & Feedback + Enemy Audio) // 3. Combat State Intensity Audio (Ambient Audio + Dynamic Music + Progression Audio) // ═══════════════════════════════════════════════════════════════════════════════════════ // ────────────────────────────────────────────────────────────────────────────── // CONSENSUS #1: ABILITY-SPECIFIC SOUND SIGNATURES // Each ability has a unique procedural audio signature that conveys its power // ────────────────────────────────────────────────────────────────────────────── const AbilitySoundSystem = { // Ability sound profiles with unique frequency patterns and characteristics signatures: { powerStrike: { name: 'Power Strike', // Heavy, punchy impact - low frequency buildup then slam frequencies: [65, 82, 110, 55], durations: [0.05, 0.08, 0.15, 0.3], volumes: [0.15, 0.2, 0.35, 0.25], delays: [0, 20, 50, 80], type: 'sawtooth', filterFreq: 400, attack: 0.01, sustain: 0.1 }, whirlwind: { name: 'Whirlwind', // Swooshing circular motion - rising frequency sweep frequencies: [220, 330, 440, 550, 660], durations: [0.1, 0.1, 0.1, 0.1, 0.15], volumes: [0.12, 0.15, 0.18, 0.15, 0.1], delays: [0, 40, 80, 120, 160], type: 'sine', filterFreq: 1200, attack: 0.02, sustain: 0.08 }, warcry: { name: 'War Cry', // Deep, resonant horn-like call frequencies: [110, 165, 220, 330], durations: [0.4, 0.35, 0.3, 0.25], volumes: [0.25, 0.2, 0.15, 0.1], delays: [0, 0, 0, 100], type: 'sawtooth', filterFreq: 600, attack: 0.05, sustain: 0.3 }, heal: { name: 'Heal', // Gentle, ascending harmony - warm and comforting frequencies: [392, 494, 587, 784], durations: [0.3, 0.25, 0.2, 0.35], volumes: [0.15, 0.12, 0.1, 0.08], delays: [0, 80, 160, 240], type: 'sine', filterFreq: 2000, attack: 0.1, sustain: 0.2 }, dash: { name: 'Dash', // Whooshing air displacement - quick frequency sweep frequencies: [200, 400, 800, 1600], durations: [0.05, 0.04, 0.03, 0.02], volumes: [0.2, 0.15, 0.1, 0.05], delays: [0, 15, 30, 45], type: 'sine', filterFreq: 2500, attack: 0.005, sustain: 0.02 }, shieldWall: { name: 'Shield Wall', // Metallic clang with resonance frequencies: [220, 440, 880, 147], durations: [0.2, 0.15, 0.1, 0.4], volumes: [0.25, 0.15, 0.08, 0.12], delays: [0, 10, 20, 30], type: 'triangle', filterFreq: 1500, attack: 0.001, sustain: 0.15 }, execute: { name: 'Execute', // Dark, ominous - low frequency with sharp attack frequencies: [55, 73, 55, 110], durations: [0.15, 0.2, 0.3, 0.1], volumes: [0.3, 0.25, 0.2, 0.15], delays: [0, 50, 100, 150], type: 'sawtooth', filterFreq: 300, attack: 0.005, sustain: 0.1 }, berserk: { name: 'Berserk', // Intense, building rage - layered dissonance frequencies: [82, 123, 164, 246, 82], durations: [0.3, 0.25, 0.2, 0.15, 0.5], volumes: [0.2, 0.22, 0.25, 0.18, 0.3], delays: [0, 50, 100, 150, 0], type: 'sawtooth', filterFreq: 500, attack: 0.02, sustain: 0.25 }, chronoEcho: { name: 'Chrono Echo', // Ethereal, time-warping - shimmering overtones frequencies: [523, 659, 784, 1047, 523], durations: [0.2, 0.18, 0.16, 0.14, 0.3], volumes: [0.1, 0.12, 0.1, 0.08, 0.06], delays: [0, 60, 120, 180, 240], type: 'sine', filterFreq: 3000, attack: 0.05, sustain: 0.15 } }, play(abilityKey) { if (!AudioSystem.enabled || !AudioSystem.ctx) return; AudioSystem.resume(); const sig = this.signatures[abilityKey]; if (!sig) return; const ctx = AudioSystem.ctx; const now = ctx.currentTime; // Create filter for character const masterFilter = ctx.createBiquadFilter(); masterFilter.type = 'lowpass'; masterFilter.frequency.value = sig.filterFreq; masterFilter.Q.value = 1.5; masterFilter.connect(ctx.destination); // Play each frequency layer sig.frequencies.forEach((freq, i) => { const delay = sig.delays[i] / 1000; const startTime = now + delay; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = sig.type; osc.frequency.setValueAtTime(freq, startTime); // Add slight pitch bend for organic feel osc.frequency.linearRampToValueAtTime(freq * 0.98, startTime + sig.durations[i] * 0.5); osc.frequency.linearRampToValueAtTime(freq * 0.95, startTime + sig.durations[i]); // ADSR envelope const vol = sig.volumes[i] * AudioSystem.masterVolume; gain.gain.setValueAtTime(0, startTime); gain.gain.linearRampToValueAtTime(vol, startTime + sig.attack); gain.gain.setValueAtTime(vol, startTime + sig.sustain); gain.gain.exponentialRampToValueAtTime(0.001, startTime + sig.durations[i]); osc.connect(gain).connect(masterFilter); osc.start(startTime); osc.stop(startTime + sig.durations[i] + 0.1); }); } }; // ────────────────────────────────────────────────────────────────────────────── // CONSENSUS #2: LAYERED IMPACT AUDIO SYSTEM // Multiple audio layers combine based on hit type, damage, and context // ────────────────────────────────────────────────────────────────────────────── const LayeredImpactAudio = { // Impact layer configurations layers: { // Base impact layer - always plays base: { light: { freq: 200, dur: 0.08, vol: 0.15, type: 'sine' }, medium: { freq: 150, dur: 0.12, vol: 0.2, type: 'triangle' }, heavy: { freq: 100, dur: 0.18, vol: 0.28, type: 'sawtooth' }, critical: { freq: 80, dur: 0.25, vol: 0.35, type: 'sawtooth' } }, // Punch layer - adds body to the hit punch: { light: { freq: 80, dur: 0.05, vol: 0.1, type: 'sine' }, medium: { freq: 60, dur: 0.08, vol: 0.15, type: 'sine' }, heavy: { freq: 45, dur: 0.12, vol: 0.22, type: 'sine' }, critical: { freq: 35, dur: 0.18, vol: 0.3, type: 'sine' } }, // Crack layer - adds sharpness crack: { light: { freq: 800, dur: 0.02, vol: 0.08, type: 'square' }, medium: { freq: 1000, dur: 0.03, vol: 0.12, type: 'square' }, heavy: { freq: 1200, dur: 0.04, vol: 0.18, type: 'square' }, critical: { freq: 1500, dur: 0.05, vol: 0.25, type: 'square' } }, // Resonance layer - adds weight and decay resonance: { light: { freq: 120, dur: 0.15, vol: 0.05, type: 'sine' }, medium: { freq: 100, dur: 0.25, vol: 0.08, type: 'sine' }, heavy: { freq: 80, dur: 0.35, vol: 0.12, type: 'sine' }, critical: { freq: 60, dur: 0.5, vol: 0.18, type: 'sine' } } }, // Determine impact weight from damage getImpactWeight(damage, isCritical = false, isFinisher = false) { if (isCritical || isFinisher) return 'critical'; if (damage >= 100) return 'heavy'; if (damage >= 30) return 'medium'; return 'light'; }, // Play layered impact sound play(damage, options = {}) { if (!AudioSystem.enabled || !AudioSystem.ctx) return; AudioSystem.resume(); const ctx = AudioSystem.ctx; const now = ctx.currentTime; const weight = this.getImpactWeight(damage, options.critical, options.finisher); // Create master filter for warmth const masterFilter = ctx.createBiquadFilter(); masterFilter.type = 'lowpass'; masterFilter.frequency.value = weight === 'critical' ? 2000 : weight === 'heavy' ? 1500 : 1200; masterFilter.connect(ctx.destination); // Play each layer with slight timing offsets for depth const layerDelays = { base: 0, punch: 0.005, crack: 0.002, resonance: 0.02 }; Object.entries(this.layers).forEach(([layerName, weights]) => { const config = weights[weight]; if (!config) return; const delay = layerDelays[layerName]; const startTime = now + delay; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = config.type; osc.frequency.setValueAtTime(config.freq, startTime); // Pitch drop for weight if (layerName === 'base' || layerName === 'punch') { osc.frequency.exponentialRampToValueAtTime(config.freq * 0.7, startTime + config.dur); } // Sharp attack, exponential decay const vol = config.vol * AudioSystem.masterVolume; gain.gain.setValueAtTime(vol, startTime); gain.gain.exponentialRampToValueAtTime(0.001, startTime + config.dur); osc.connect(gain).connect(masterFilter); osc.start(startTime); osc.stop(startTime + config.dur + 0.05); }); // Add special effects for critical/finisher hits if (weight === 'critical') { this.playCriticalSweetener(now); } }, // Extra sparkle for critical hits playCriticalSweetener(now) { if (!AudioSystem.ctx) return; const ctx = AudioSystem.ctx; // High shimmer const shimmer = ctx.createOscillator(); const shimmerGain = ctx.createGain(); shimmer.type = 'sine'; shimmer.frequency.setValueAtTime(2000, now); shimmer.frequency.linearRampToValueAtTime(3000, now + 0.1); shimmerGain.gain.setValueAtTime(AudioSystem.masterVolume * 0.08, now); shimmerGain.gain.exponentialRampToValueAtTime(0.001, now + 0.15); shimmer.connect(shimmerGain).connect(ctx.destination); shimmer.start(now + 0.02); shimmer.stop(now + 0.2); // Sub bass thump const sub = ctx.createOscillator(); const subGain = ctx.createGain(); sub.type = 'sine'; sub.frequency.value = 40; subGain.gain.setValueAtTime(AudioSystem.masterVolume * 0.25, now); subGain.gain.exponentialRampToValueAtTime(0.001, now + 0.2); sub.connect(subGain).connect(ctx.destination); sub.start(now); sub.stop(now + 0.25); }, // Enemy death sound - more elaborate playDeath(enemyType = 'normal') { if (!AudioSystem.enabled || !AudioSystem.ctx) return; AudioSystem.resume(); const ctx = AudioSystem.ctx; const now = ctx.currentTime; // Death is always dramatic this.play(150, { finisher: true }); // Add death-specific elements // Descending tone (soul departing) const soul = ctx.createOscillator(); const soulGain = ctx.createGain(); soul.type = 'sine'; soul.frequency.setValueAtTime(400, now + 0.1); soul.frequency.exponentialRampToValueAtTime(100, now + 0.6); soulGain.gain.setValueAtTime(AudioSystem.masterVolume * 0.12, now + 0.1); soulGain.gain.exponentialRampToValueAtTime(0.001, now + 0.6); soul.connect(soulGain).connect(ctx.destination); soul.start(now + 0.1); soul.stop(now + 0.7); // Boss death gets extra fanfare if (enemyType === 'boss') { setTimeout(() => AudioSystem.victoryFanfare(), 200); } } }; // ────────────────────────────────────────────────────────────────────────────── // CONSENSUS #3: COMBAT STATE INTENSITY AUDIO // Dynamic audio that responds to combat tension and game state // ────────────────────────────────────────────────────────────────────────────── const CombatIntensityAudio = { currentIntensity: 0, // 0-100 intensity scale targetIntensity: 0, tensionNodes: null, isActive: false, lastUpdate: 0, updateInterval: 100, // ms between intensity checks // Intensity thresholds for different audio behaviors thresholds: { calm: 10, // Below this: peaceful ambient alert: 30, // Combat nearby but not engaged combat: 50, // Active combat intense: 75, // Multiple enemies, taking damage critical: 90 // Boss fight or low health in combat }, // Calculate intensity from game state // v7.77: Use distanceToSquared to eliminate sqrt calls in hot path // v8.03: Converted forEach to for loop for performance calculateIntensity() { let intensity = 0; // Factor 1: Number of nearby enemies // v7.77: Pre-compute squared thresholds to avoid sqrt per mob const NEARBY_DIST_SQ = 30 * 30; // 900 const CLOSE_DIST_SQ = 10 * 10; // 100 if (typeof worldState !== 'undefined' && worldState.mobs && worldState.player) { const playerPos = worldState.player.position; const mobs = worldState.mobs; let nearbyEnemies = 0; let veryCloseEnemies = 0; for (let i = 0, len = mobs.length; i < len; i++) { const mob = mobs[i]; if (!mob.position) continue; const distSq = mob.position.distanceToSquared(playerPos); if (distSq < NEARBY_DIST_SQ) nearbyEnemies++; if (distSq < CLOSE_DIST_SQ) veryCloseEnemies++; } intensity += nearbyEnemies * 8; intensity += veryCloseEnemies * 15; } // Factor 2: Player health percentage if (typeof gameData !== 'undefined' && gameData.player) { const hpPercent = gameData.player.hp / gameData.player.maxHp; if (hpPercent < 0.25) intensity += 30; else if (hpPercent < 0.5) intensity += 15; else if (hpPercent < 0.75) intensity += 5; } // Factor 3: Recent combat activity (combo state) if (typeof comboState !== 'undefined' && comboState.active) { intensity += 20 + comboState.count * 5; } // Factor 4: Style meter if (typeof styleMeterState !== 'undefined') { intensity += styleMeterState.score / 20; // Max ~50 from style } // Factor 5: Active buffs (war cry, berserk) if (typeof abilityState !== 'undefined') { const now = performance.now(); if (abilityState.warcry?.activeUntil > now) intensity += 10; if (abilityState.berserk?.activeUntil > now) intensity += 25; } return Math.min(100, Math.max(0, intensity)); }, // Update intensity smoothly update() { const now = performance.now(); if (now - this.lastUpdate < this.updateInterval) return; this.lastUpdate = now; this.targetIntensity = this.calculateIntensity(); // Smooth interpolation - ramp up faster than down const rampSpeed = this.targetIntensity > this.currentIntensity ? 0.15 : 0.05; this.currentIntensity += (this.targetIntensity - this.currentIntensity) * rampSpeed; // Update tension drone based on intensity this.updateTensionDrone(); }, // Dynamic tension drone that pulses with combat intensity updateTensionDrone() { if (!AudioSystem.enabled || !AudioSystem.ctx) return; const intensity = this.currentIntensity; // Start/stop tension system based on threshold if (intensity > this.thresholds.alert && !this.isActive) { this.startTensionSystem(); } else if (intensity < this.thresholds.calm && this.isActive) { this.stopTensionSystem(); } // Modulate existing tension if (this.isActive && this.tensionNodes) { const normalizedIntensity = intensity / 100; // Volume scales with intensity const targetVol = normalizedIntensity * 0.08 * AudioSystem.masterVolume; this.tensionNodes.gain.gain.linearRampToValueAtTime( targetVol, AudioSystem.ctx.currentTime + 0.1 ); // Filter opens up at higher intensity const filterFreq = 100 + normalizedIntensity * 300; this.tensionNodes.filter.frequency.linearRampToValueAtTime( filterFreq, AudioSystem.ctx.currentTime + 0.1 ); // LFO speed increases with intensity const lfoSpeed = 0.5 + normalizedIntensity * 2; this.tensionNodes.lfo.frequency.linearRampToValueAtTime( lfoSpeed, AudioSystem.ctx.currentTime + 0.1 ); } }, startTensionSystem() { if (this.isActive || !AudioSystem.ctx) return; AudioSystem.resume(); const ctx = AudioSystem.ctx; this.isActive = true; // Create tension drone oscillator const osc = ctx.createOscillator(); osc.type = 'sawtooth'; osc.frequency.value = 55; // Low A // Sub oscillator for depth const subOsc = ctx.createOscillator(); subOsc.type = 'sine'; subOsc.frequency.value = 27.5; // Sub bass // LFO for pulsing const lfo = ctx.createOscillator(); const lfoGain = ctx.createGain(); lfo.type = 'sine'; lfo.frequency.value = 1; lfoGain.gain.value = 20; lfo.connect(lfoGain).connect(osc.frequency); // Filter for warmth and intensity control const filter = ctx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 150; filter.Q.value = 2; // Master gain const gain = ctx.createGain(); gain.gain.value = 0; // Connect osc.connect(filter); subOsc.connect(filter); filter.connect(gain).connect(ctx.destination); // Start osc.start(); subOsc.start(); lfo.start(); this.tensionNodes = { osc, subOsc, lfo, filter, gain }; }, stopTensionSystem() { if (!this.isActive || !this.tensionNodes) return; const ctx = AudioSystem.ctx; const now = ctx.currentTime; // Fade out this.tensionNodes.gain.gain.linearRampToValueAtTime(0, now + 0.5); // Schedule cleanup setTimeout(() => { if (this.tensionNodes) { try { this.tensionNodes.osc.stop(); this.tensionNodes.subOsc.stop(); this.tensionNodes.lfo.stop(); this.tensionNodes.osc.disconnect(); this.tensionNodes.subOsc.disconnect(); this.tensionNodes.lfo.disconnect(); } catch (e) {} this.tensionNodes = null; } }, 600); this.isActive = false; }, // Play intensity-aware stingers for key moments playStinger(type) { if (!AudioSystem.enabled || !AudioSystem.ctx) return; AudioSystem.resume(); const ctx = AudioSystem.ctx; const now = ctx.currentTime; const intensityMod = 0.5 + (this.currentIntensity / 200); // 0.5-1.0 const stingers = { // Combat start engage: () => { [110, 165, 220].forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.value = freq; gain.gain.setValueAtTime(AudioSystem.masterVolume * 0.15 * intensityMod, now + i * 0.05); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.3 + i * 0.05); osc.connect(gain).connect(ctx.destination); osc.start(now + i * 0.05); osc.stop(now + 0.4); }); }, // Wave clear clear: () => { [220, 330, 440, 550].forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; gain.gain.setValueAtTime(0, now + i * 0.08); gain.gain.linearRampToValueAtTime(AudioSystem.masterVolume * 0.12, now + i * 0.08 + 0.02); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4 + i * 0.08); osc.connect(gain).connect(ctx.destination); osc.start(now + i * 0.08); osc.stop(now + 0.5 + i * 0.08); }); }, // Danger warning danger: () => { [200, 150, 200, 150].forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'square'; osc.frequency.value = freq; gain.gain.setValueAtTime(AudioSystem.masterVolume * 0.1, now + i * 0.12); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1 + i * 0.12); osc.connect(gain).connect(ctx.destination); osc.start(now + i * 0.12); osc.stop(now + 0.15 + i * 0.12); }); } }; if (stingers[type]) stingers[type](); } }; // --- PARTICLE SYSTEM (v6.6: Performance optimized with shared geometry - Agent 1 & 5 consensus) --- class ParticleSystem { constructor() { this.particles = []; this.maxParticles = 300; // Increased capacity due to better performance this.particlePool = []; // Object pool for reuse // v6.32: Reduced geometry complexity for better performance (3,3 is sufficient for small particles) this.sharedGeometry = new THREE.SphereGeometry(0.2, 3, 3); // Pre-allocated temp vector for physics calculations this._tempVelocity = new THREE.Vector3(); // v10.2: MATERIAL CACHE BY COLOR (8-Agent Consensus Cycle 3) // Eliminates setHex() allocations - reuse materials by color hex this.materialCache = {}; // v6.32: Frame rate tracking for adaptive quality this.frameCount = 0; this.lastFpsCheck = performance.now(); this.currentFps = 60; // v7.83: Pre-warm pool with common particle count to avoid allocations during gameplay this._prewarmPool(); } // v7.83: Pre-allocate particles to pool to reduce runtime allocations _prewarmPool() { const prewarmCount = 40; // Pre-create 40 particles const defaultMaterial = this.getCachedMaterial(0xffffff); for (let i = 0; i < prewarmCount; i++) { const particle = { mesh: new THREE.Mesh(this.sharedGeometry, defaultMaterial), velocity: new THREE.Vector3() }; particle.mesh.visible = false; this.particlePool.push(particle); } } // v10.2: Get or create cached material for color getCachedMaterial(color) { if (!this.materialCache[color]) { this.materialCache[color] = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 1 }); } return this.materialCache[color]; } emit(position, count, color, options = {}) { const spread = options.spread || 3; const lifetime = options.lifetime || 1000; const size = options.size || 0.2; const gravity = options.gravity !== undefined ? options.gravity : 10; // v7.99: Support offset to avoid clone().add() allocations at call sites const offsetX = options.offsetX || 0; const offsetY = options.offsetY || 0; const offsetZ = options.offsetZ || 0; // v10.2: Get cached material (no allocation if exists) const cachedMaterial = this.getCachedMaterial(color); for (let i = 0; i < count && this.particles.length < this.maxParticles; i++) { // Try to reuse from pool first let particle = this.particlePool.pop(); if (particle) { // v10.2: Use cached material instead of setHex particle.mesh.material = cachedMaterial; particle.mesh.material.opacity = 1; particle.mesh.visible = true; particle.velocity.set( (Math.random() - 0.5) * spread, Math.random() * spread * 0.8 + spread * 0.2, (Math.random() - 0.5) * spread ); } else { // Create new particle with cached material particle = { mesh: new THREE.Mesh( this.sharedGeometry, // Use shared geometry cachedMaterial // v10.2: Use cached material ), velocity: new THREE.Vector3( (Math.random() - 0.5) * spread, Math.random() * spread * 0.8 + spread * 0.2, (Math.random() - 0.5) * spread ) }; scene.add(particle.mesh); } particle.lifetime = lifetime; particle.startTime = performance.now(); particle.gravity = gravity; particle.baseSize = size; particle.mesh.scale.setScalar(size / 0.2); // Scale relative to shared geometry size // v7.99: Apply offset directly instead of requiring clone().add() at call site particle.mesh.position.set( position.x + offsetX, position.y + 1 + offsetY, position.z + offsetZ ); this.particles.push(particle); } } update(dt) { const now = performance.now(); // v6.32: Track FPS for adaptive quality this.frameCount++; if (now - this.lastFpsCheck >= 1000) { this.currentFps = this.frameCount; this.frameCount = 0; this.lastFpsCheck = now; // Adaptive quality: reduce max particles if FPS drops if (this.currentFps < 30 && this.maxParticles > 50) { this.maxParticles = Math.max(50, this.maxParticles - 25); } else if (this.currentFps > 55 && this.maxParticles < 300) { this.maxParticles = Math.min(300, this.maxParticles + 10); } } // v10.1: IN-PLACE ARRAY COMPACTION (8-Agent Consensus Cycle 2) // Avoids GC by reusing array instead of .filter() creating new one let writeIdx = 0; for (let i = 0; i < this.particles.length; i++) { const p = this.particles[i]; const elapsed = now - p.startTime; const progress = elapsed / p.lifetime; if (progress >= 1) { // Return to pool instead of disposing p.mesh.visible = false; this.particlePool.push(p); continue; // Don't keep this particle } // Physics - use pre-allocated temp vector instead of clone() p.velocity.y -= p.gravity * dt; this._tempVelocity.copy(p.velocity).multiplyScalar(dt); p.mesh.position.add(this._tempVelocity); p.mesh.material.opacity = 1 - progress; const baseScale = p.baseSize / 0.2; p.mesh.scale.setScalar(baseScale * (1 - progress * 0.5)); // Keep particle - compact in place this.particles[writeIdx++] = p; } this.particles.length = writeIdx; } } let particles; // v4.4: Hit-Stop System - Freezes game briefly on impacts for satisfying combat // v7.2: Enhanced with combo-scaling and chromatic aberration (8-Strategy Consensus Round 1) let hitStopUntil = 0; const HIT_STOP_LIGHT = 30; // Normal hits (ms) const HIT_STOP_HEAVY = 80; // Kills (ms) const HIT_STOP_BOSS = 150; // Boss impacts (ms) // v7.2: Enhanced Hit-Lag Configuration (8-Strategy Consensus Round 1) const HITLAG_CONFIG = { // Combo-scaled hit-stop (escalating feel) COMBO_BASE: 25, // Base hit-stop (slightly lower than normal) COMBO_PER_HIT: 8, // Additional ms per combo level FINISHER_MULT: 2.5, // Finisher hit-stop multiplier MAX_NORMAL: 70, // Cap for non-finisher hits // Visual effects during freeze CHROMA_SHIFT: 2, // Pixel offset for chromatic aberration // Cooldown to prevent stuttering MIN_BETWEEN_FREEZES: 80 }; let hitlagState = { lastFreezeTime: 0, chromaActive: false }; function triggerHitStop(duration) { hitStopUntil = performance.now() + duration; } // v7.2: Combo-aware hit-lag with visual effects (8-Strategy Consensus Round 1) function triggerEnhancedHitlag(damage, isFinisher = false, isCritical = false) { const now = performance.now(); const config = HITLAG_CONFIG; // Prevent stutter from rapid hits if (now - hitlagState.lastFreezeTime < config.MIN_BETWEEN_FREEZES) { return; } // Calculate duration based on combo and finisher let duration; const comboLevel = comboState?.count || 0; if (isFinisher) { duration = Math.min( (config.COMBO_BASE + comboLevel * config.COMBO_PER_HIT) * config.FINISHER_MULT, HIT_STOP_BOSS ); } else { // Normal hits scale with combo duration = Math.min( config.COMBO_BASE + comboLevel * config.COMBO_PER_HIT, config.MAX_NORMAL ); } // Critical hits get bonus if (isCritical) { duration = Math.min(duration * 1.3, HIT_STOP_BOSS); // v7.70: Trigger Time Dilation on critical hits if (typeof TimeDilationSystem !== 'undefined') { TimeDilationSystem.trigger('criticalHit'); } } // v7.70: Finishers get epic slow-mo if (isFinisher && typeof TimeDilationSystem !== 'undefined') { TimeDilationSystem.trigger('killConfirm'); } hitlagState.lastFreezeTime = now; triggerHitStop(duration); // Apply chromatic aberration for heavy hits if (duration > 40 || isFinisher) { applyHitlagChroma(duration, isFinisher); } } // v7.2: Chromatic aberration effect during hit-lag // v7.3: Enhanced with CSS overlay and impact border integration (8-Strategy Consensus) function applyHitlagChroma(duration, isFinisher, isBossHit = false) { if (hitlagState.chromaActive) return; hitlagState.chromaActive = true; // Trigger chromatic aberration overlay const chromaOverlay = document.getElementById('chromatic-aberration'); if (chromaOverlay) { chromaOverlay.classList.remove('active'); void chromaOverlay.offsetWidth; // Force reflow chromaOverlay.classList.add('active'); } // Trigger enhanced impact border effect const impactBorder = document.getElementById('impact-border'); if (impactBorder) { // Remove all impact classes impactBorder.classList.remove('damage-dealt', 'critical-hit', 'boss-slam', 'finisher-hit'); void impactBorder.offsetWidth; // Force reflow // Apply appropriate effect class if (isBossHit) { impactBorder.classList.add('boss-slam'); } else if (isFinisher) { impactBorder.classList.add('finisher-hit'); } else { impactBorder.classList.add('critical-hit'); } } // Also apply subtle container effects for extra juice const container = document.getElementById('container'); if (container) { const intensity = isFinisher ? 3 : 2; container.style.filter = ` drop-shadow(${intensity}px 0 0 rgba(255,80,80,0.25)) drop-shadow(-${intensity}px 0 0 rgba(80,200,255,0.25)) `; container.style.transform = `scale(${1 + (isFinisher ? 0.008 : 0.004)})`; container.style.transition = 'transform 0.05s ease-out, filter 0.05s ease-out'; } // Clear after hit-lag setTimeout(() => { if (container) { container.style.filter = ''; container.style.transform = ''; container.style.transition = ''; } if (chromaOverlay) { chromaOverlay.classList.remove('active'); } hitlagState.chromaActive = false; }, duration + 100); } // ============================================ // v7.70: TIME DILATION SYSTEM (8-Agent Consensus Cycle 45) // Slow-motion on epic moments for maximum impact // ============================================ const TimeDilationSystem = { active: false, targetScale: 1.0, currentScale: 1.0, dilationEnd: 0, overlay: null, config: { criticalHit: { scale: 0.25, duration: 180, blur: true }, perfectParry: { scale: 0.20, duration: 200, blur: true }, killConfirm: { scale: 0.15, duration: 250, blur: true }, bossHit: { scale: 0.30, duration: 150, blur: true }, clutchMoment: { scale: 0.10, duration: 300, blur: true } }, init() { if (this.overlay) return; this.overlay = document.createElement('div'); this.overlay.id = 'time-dilation-overlay'; this.overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 9999; opacity: 0; background: radial-gradient(ellipse at center, rgba(0,0,0,0) 0%, rgba(0,0,0,0.3) 70%, rgba(0,0,0,0.6) 100%); transition: opacity 0.05s ease-out; `; document.body.appendChild(this.overlay); }, trigger(type = 'criticalHit') { const cfg = this.config[type] || this.config.criticalHit; this.init(); this.active = true; this.targetScale = cfg.scale; this.dilationEnd = performance.now() + cfg.duration; if (cfg.blur && this.overlay) { this.overlay.style.opacity = '1'; this.overlay.style.backdropFilter = 'blur(2px)'; } if (typeof MobileHaptics !== 'undefined') MobileHaptics.vibrateCustom([100, 50, 100]); }, update() { if (!this.active) return 1.0; const now = performance.now(); if (now >= this.dilationEnd) { this.currentScale = Math.min(this.currentScale + 0.1, 1.0); if (this.currentScale >= 1.0) { this.active = false; this.currentScale = 1.0; if (this.overlay) { this.overlay.style.opacity = '0'; this.overlay.style.backdropFilter = ''; } } } else { this.currentScale = this.targetScale; } return this.currentScale; }, getTimeScale() { return this.active ? this.currentScale : 1.0; } }; // ============================================ // v7.70: DEATH ANALYTICS SYSTEM (8-Agent Consensus Cycle 45) // Transform rage-quits into learning moments // ============================================ const DeathAnalyticsSystem = { combatLog: [], maxLogEntries: 50, logDamage(source, amount) { this.combatLog.push({ time: performance.now(), source, amount }); if (this.combatLog.length > this.maxLogEntries) this.combatLog.shift(); }, onPlayerDeath() { const recent = this.combatLog.filter(l => performance.now() - l.time < 10000); if (recent.length === 0) return; const killingBlow = recent[recent.length - 1]; const damageBySource = {}; let total = 0; recent.forEach(l => { damageBySource[l.source] = (damageBySource[l.source] || 0) + l.amount; total += l.amount; }); let topSource = '', topDmg = 0; for (const [src, dmg] of Object.entries(damageBySource)) { if (dmg > topDmg) { topDmg = dmg; topSource = src; } } this.showDeathScreen(killingBlow, total, topSource); }, getTip(src) { const tips = { 'Tower': 'Use creep waves as cover! Towers prioritize creeps.', 'Creep': 'Farm with last-hits. Don\'t tank creep waves.', 'Enemy': 'Watch positioning. Stay behind your creeps.', 'Boss': 'Watch for telegraphed attacks.', 'default': 'Retreat when HP is low!' }; return tips[src] || tips['default']; }, showDeathScreen(kill, total, topSrc) { const overlay = document.createElement('div'); overlay.id = 'death-analytics'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.9); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 10000; color: white; font-family: sans-serif; `; overlay.innerHTML = `

💀 DEFEATED 💀

Killing Blow

${kill.source} - ${Math.round(kill.amount)} dmg

💥
${Math.round(total)}
Total Damage
🎯
${topSrc}
Top Threat
💡 TIP
${this.getTip(topSrc)}
`; document.body.appendChild(overlay); this.combatLog = []; } }; // ============================================ // v7.2: COMBAT POINT LIGHT SYSTEM (8-Strategy Consensus Round 2) // Dynamic lights that flash on hits, abilities, and explosions // ============================================ const CombatLightSystem = { activeLights: [], maxLights: 8, lightPool: [], initialized: false, init() { if (this.initialized || !scene) return; // Pre-create pooled lights for (let i = 0; i < this.maxLights; i++) { const light = new THREE.PointLight(0xffffff, 0, 12); light.visible = false; scene.add(light); this.lightPool.push(light); } this.initialized = true; }, // Flash light at position (for hits, explosions) flash(position, color = 0xff8844, intensity = 2.5, duration = 200, radius = 10) { if (!this.initialized) this.init(); if (this.activeLights.length >= this.maxLights) return; const light = this.lightPool.find(l => !l.visible); if (!light) return; light.position.copy(position); light.position.y += 1.5; light.color.setHex(color); light.intensity = intensity; light.distance = radius; light.visible = true; const startTime = performance.now(); this.activeLights.push({ light, startTime, duration, intensity }); }, // Sustained glow for abilities glow(position, color, intensity, duration, radius = 15) { this.flash(position, color, intensity * 0.7, duration, radius); }, update() { const now = performance.now(); for (let i = this.activeLights.length - 1; i >= 0; i--) { const al = this.activeLights[i]; const elapsed = now - al.startTime; const progress = elapsed / al.duration; if (progress >= 1) { al.light.visible = false; al.light.intensity = 0; this.activeLights.splice(i, 1); } else { // Smooth ease-out fade al.light.intensity = al.intensity * (1 - progress * progress); } } } }; // Light colors by ability/context const COMBAT_LIGHT_COLORS = { hit: 0xff8844, // Orange for normal hits criticalHit: 0xffd700, // Gold for crits playerDamage: 0xff2222, // Red when player takes damage powerStrike: 0xffaa00, // Golden for power strike whirlwind: 0x00ffff, // Cyan for whirlwind heal: 0x44ff88, // Green for healing lightning: 0xffffaa, // Bright yellow explosion: 0xff4400, // Deep orange death: 0xff00ff // Purple for enemy death }; // ============================================ // v7.2: SLASH TRAIL SYSTEM (8-Strategy Consensus Round 2) // Ribbon mesh trails during attacks for visual spectacle // ============================================ const SlashTrailSystem = { trails: [], maxTrails: 3, isAttacking: false, currentTrail: null, // v7.89: Pooled geometry for impact rings _impactRingGeometry: null, // v7.89: Pre-allocated vectors for addPoint calculations _tempUp: null, _tempHalfWidth: null, _tempLookAt: null, createTrail(color = 0x00ffff) { if (!scene) return null; const maxPoints = 16; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(maxPoints * 2 * 3); const colors = new Float32Array(maxPoints * 2 * 4); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4)); const material = new THREE.MeshBasicMaterial({ vertexColors: true, transparent: true, side: THREE.DoubleSide, blending: THREE.AdditiveBlending, depthWrite: false }); const mesh = new THREE.Mesh(geometry, material); mesh.userData = { points: [], maxPoints: maxPoints, color: new THREE.Color(color), lifetime: 300, startTime: performance.now(), fading: false }; scene.add(mesh); return mesh; }, startTrail(color) { if (this.trails.length >= this.maxTrails) { // Remove oldest const oldest = this.trails.shift(); if (oldest && oldest.parent) scene.remove(oldest); } this.currentTrail = this.createTrail(color); if (this.currentTrail) { this.trails.push(this.currentTrail); this.isAttacking = true; } }, addPoint(position, upVector) { if (!this.currentTrail || !this.isAttacking) return; const trail = this.currentTrail; const width = 0.6; // v7.89: Use pooled vectors for calculations if (!this._tempUp) this._tempUp = new THREE.Vector3(0, 1, 0); if (!this._tempHalfWidth) this._tempHalfWidth = new THREE.Vector3(); // Add two vertices (top and bottom of ribbon) if (upVector) { this._tempUp.copy(upVector); } else { this._tempUp.set(0, 1, 0); } this._tempHalfWidth.copy(this._tempUp).multiplyScalar(width / 2); // v7.97: Use GlobalVec3Pool.acquire() for persistent trail points (they need to outlive this call) // This eliminates 2 clone() allocations per addPoint call (~60/sec during combat) const topVec = GlobalVec3Pool.acquire().copy(position).add(this._tempHalfWidth); const bottomVec = GlobalVec3Pool.acquire().copy(position).sub(this._tempHalfWidth); trail.userData.points.push({ top: topVec, bottom: bottomVec, time: performance.now() }); // Limit points if (trail.userData.points.length > trail.userData.maxPoints) { // v7.97: Release pooled vectors back to pool before removing const removed = trail.userData.points.shift(); if (removed) { GlobalVec3Pool.release(removed.top); GlobalVec3Pool.release(removed.bottom); } } this.updateTrailGeometry(trail); }, updateTrailGeometry(trail) { const points = trail.userData.points; if (points.length < 2) return; const positions = trail.geometry.attributes.position.array; const colors = trail.geometry.attributes.color.array; const color = trail.userData.color; for (let i = 0; i < points.length; i++) { const p = points[i]; const alpha = i / (points.length - 1); // Fade from tail to head // Position (two vertices per point) const pi = i * 6; positions[pi] = p.top.x; positions[pi + 1] = p.top.y; positions[pi + 2] = p.top.z; positions[pi + 3] = p.bottom.x; positions[pi + 4] = p.bottom.y; positions[pi + 5] = p.bottom.z; // Color with alpha gradient const ci = i * 8; colors[ci] = color.r; colors[ci + 1] = color.g; colors[ci + 2] = color.b; colors[ci + 3] = alpha * 0.8; colors[ci + 4] = color.r; colors[ci + 5] = color.g; colors[ci + 6] = color.b; colors[ci + 7] = alpha * 0.8; } trail.geometry.attributes.position.needsUpdate = true; trail.geometry.attributes.color.needsUpdate = true; trail.geometry.setDrawRange(0, points.length * 2); }, endTrail() { if (this.currentTrail) { this.currentTrail.userData.fading = true; this.currentTrail.userData.fadeStart = performance.now(); } this.isAttacking = false; this.currentTrail = null; }, update(dt) { const now = performance.now(); this.trails = this.trails.filter(trail => { if (!trail.parent) return false; if (trail.userData.fading) { const fadeElapsed = now - trail.userData.fadeStart; const fadeProgress = fadeElapsed / 200; // 200ms fade if (fadeProgress >= 1) { // v7.97: Release all pooled vectors before disposing trail if (trail.userData.points) { for (const pt of trail.userData.points) { GlobalVec3Pool.release(pt.top); GlobalVec3Pool.release(pt.bottom); } trail.userData.points.length = 0; } scene.remove(trail); trail.geometry.dispose(); trail.material.dispose(); return false; } // Fade all vertices const colors = trail.geometry.attributes.color.array; for (let i = 3; i < colors.length; i += 4) { colors[i] *= 0.9; } trail.geometry.attributes.color.needsUpdate = true; } return true; }); }, // Spawn impact geometry at hit location spawnImpact(position, color = 0xffffff) { if (!scene) return; // v7.89: Use pooled geometry (shared across all impact rings) if (!this._impactRingGeometry) { this._impactRingGeometry = new THREE.RingGeometry(0.1, 0.3, 16); } // v7.89: Lazy-init lookAt temp vector if (!this._tempLookAt) this._tempLookAt = new THREE.Vector3(); // Create expanding ring - material must be unique per ring for opacity animation const ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.8, side: THREE.DoubleSide, blending: THREE.AdditiveBlending }); const ring = new THREE.Mesh(this._impactRingGeometry, ringMat); ring.position.copy(position); // v7.89: Use pooled vector for lookAt fallback if (camera) { ring.lookAt(camera.position); } else { this._tempLookAt.copy(position).z += 1; ring.lookAt(this._tempLookAt); } scene.add(ring); // Animate expansion and fade const startTime = performance.now(); const animate = () => { // v8.34: Skip animation when tab is hidden if (!isPageVisible) { requestAnimationFrame(animate); return; } const elapsed = performance.now() - startTime; const t = elapsed / 250; // 250ms animation if (t >= 1) { scene.remove(ring); // v7.89: Don't dispose pooled geometry, only dispose material ringMat.dispose(); return; } const scale = 1 + t * 3; ring.scale.set(scale, scale, scale); ringMat.opacity = 0.8 * (1 - t); requestAnimationFrame(animate); }; animate(); } }; // ============================================ // v7.2: ENEMY DEATH DISSOLUTION SYSTEM (8-Strategy Consensus Round 2) // Animated dissolve effect when enemies die // ============================================ const DeathDissolutionSystem = { dissolving: [], maxDissolving: 5, trigger(mob, mobColor) { if (!mob || !scene) return; if (this.dissolving.length >= this.maxDissolving) return; if (mob.userData?.isDissolving) return; mob.userData.isDissolving = true; mob.userData.dissolveData = { startTime: performance.now(), duration: 500, originalColor: mobColor || 0xff4444, particles: [], phase: 0 }; this.dissolving.push(mob); // Hide HP bar immediately if (mob.userData.hpBar) { mob.userData.hpBar.visible = false; } // Create dissolution particles // v7.90: Pre-compute particle positions to avoid clone()+new Vector3() in setTimeout closures const particleCount = 10; const particlePositions = []; for (let i = 0; i < particleCount; i++) { const angle = (i / particleCount) * Math.PI * 2; // Pre-compute the offset-applied position now (before setTimeout) particlePositions.push({ x: mob.position.x + Math.cos(angle) * 0.5, y: mob.position.y + Math.random() * 1.5, z: mob.position.z + Math.sin(angle) * 0.5 }); } // v7.90: Single temp vector for emitting (reused across all setTimeout calls) const _emitPos = GlobalVec3Pool.acquire(); for (let i = 0; i < particleCount; i++) { if (particles) { const precomputed = particlePositions[i]; setTimeout(() => { if (mob.position) { _emitPos.set(precomputed.x, precomputed.y, precomputed.z); particles.emit( _emitPos, 3, mobColor || 0xff4444, { spread: 1.5, lifetime: 400, gravity: -2 } ); } // Release on last particle if (i === particleCount - 1) { GlobalVec3Pool.release(_emitPos); } }, i * 30); } } // Flash combat light if (CombatLightSystem.initialized) { CombatLightSystem.flash(mob.position, COMBAT_LIGHT_COLORS.death, 2, 300, 8); } }, update(dt) { const now = performance.now(); this.dissolving = this.dissolving.filter(mob => { if (!mob || !mob.parent) return false; const data = mob.userData?.dissolveData; if (!data) return false; const elapsed = now - data.startTime; const t = Math.min(1, elapsed / data.duration); // Phase 1 (0-20%): Scale pulse with white flash if (t < 0.2) { const pulseT = t / 0.2; const scale = 1 + Math.sin(pulseT * Math.PI) * 0.15; mob.scale.setScalar(scale); // White emissive flash mob.traverse(child => { if (child.material?.emissive) { child.material.emissive.setHex(0xffffff); child.material.emissiveIntensity = 2 * (1 - pulseT); } }); } // Phase 2 (20-90%): Shrink and spin else if (t < 0.9) { const shrinkT = (t - 0.2) / 0.7; const scale = 1 - shrinkT * 0.8; mob.scale.setScalar(Math.max(0.1, scale)); mob.rotation.y += dt * 8; mob.position.y += dt * 2; // Rise slightly // Fade material mob.traverse(child => { if (child.material) { child.material.transparent = true; child.material.opacity = 1 - shrinkT; } }); } // Phase 3 (90-100%): Final burst and removal else { // Final particle burst if (!data.finalBurst) { data.finalBurst = true; if (particles && mob.position) { particles.emit(mob.position, 15, data.originalColor, { spread: 3, lifetime: 600, gravity: -1 }); } } if (t >= 1) { scene.remove(mob); return false; } } return true; }); } }; // ============================================ // v7.23: ENHANCED DEATH IMPACT SYSTEM (8-Strategy Consensus Cycle 8) // Dramatically improves the "pop" on enemy death: // - Scale burst (rapid expansion then shrink) // - Directional particle explosion // - Brief time dilation (50-80ms slowdown) // - Death lighting flash centered on mob // - Screen compression effect // ============================================ const EnhancedDeathImpact = { // v7.89: Pre-allocated vectors for particle ring positions _tempRingOffset: null, _tempPos: null, // Config for different enemy types config: { normal: { scaleBurst: 1.4, // Max scale during burst burstDuration: 80, // ms for scale burst particleCount: 25, // Particle explosion count timeDilation: 0.3, // Time scale (0.3 = 30% speed) dilationDuration: 60, // ms of slow-mo lightIntensity: 3, lightRadius: 8, screenFlash: true }, elite: { scaleBurst: 1.6, burstDuration: 100, particleCount: 40, timeDilation: 0.2, dilationDuration: 100, lightIntensity: 5, lightRadius: 12, screenFlash: true }, boss: { scaleBurst: 1.8, burstDuration: 150, particleCount: 80, timeDilation: 0.1, dilationDuration: 200, lightIntensity: 8, lightRadius: 20, screenFlash: true } }, // Active death effects activeEffects: [], maxConcurrent: 5, // Trigger enhanced death effect on mob trigger(mob, killType = 'normal', mobColor = 0xff4444) { if (!mob || !mob.position) return; if (this.activeEffects.length >= this.maxConcurrent) return; // v7.32: 3D spatial death audio (Cycle 5 Consensus) if (typeof SpatialAudioSystem !== 'undefined' && SpatialAudioSystem.ctx && mob?.position) { SpatialAudioSystem.playDeath3D(mob.position); } // v7.89: Lazy-init pooled vectors if (!this._tempRingOffset) this._tempRingOffset = new THREE.Vector3(); if (!this._tempPos) this._tempPos = new THREE.Vector3(); const cfg = this.config[killType] || this.config.normal; // Note: pos needs to be captured for setTimeout closures, but we use pooled _tempPos for ring calculations const pos = mob.position.clone(); // 1. SCALE BURST - rapid scale-up then scale-down if (mob.scale) { const originalScale = mob.scale.x; const startTime = performance.now(); const animateScaleBurst = () => { const elapsed = performance.now() - startTime; const t = Math.min(1, elapsed / cfg.burstDuration); if (t < 0.3) { // Rapid expansion phase const expandT = t / 0.3; const scale = originalScale * (1 + (cfg.scaleBurst - 1) * expandT); mob.scale.setScalar(scale); } else { // Quick shrink phase const shrinkT = (t - 0.3) / 0.7; const scale = originalScale * cfg.scaleBurst * (1 - shrinkT * 0.5); mob.scale.setScalar(Math.max(0.1, scale)); } if (t < 1 && mob.parent) { requestAnimationFrame(animateScaleBurst); } }; animateScaleBurst(); } // 2. DIRECTIONAL PARTICLE EXPLOSION - outward burst from death position if (typeof particles !== 'undefined' && particles) { // Primary burst - outward in all directions particles.emit(pos, cfg.particleCount, mobColor, { spread: killType === 'boss' ? 10 : killType === 'elite' ? 7 : 5, lifetime: killType === 'boss' ? 1200 : 800, gravity: -1.5, size: killType === 'boss' ? 0.4 : killType === 'elite' ? 0.3 : 0.2 }); // Secondary golden sparkle burst for special kills if (killType !== 'normal') { setTimeout(() => { particles.emit(pos, Math.floor(cfg.particleCount * 0.5), 0xffd700, { spread: cfg.lightRadius * 0.6, lifetime: 1000, gravity: -2, size: 0.25 }); }, 30); } // Ring of particles expanding outward // v7.89: Pre-calculate ring positions to avoid creating vectors in setTimeout closures const ringCount = killType === 'boss' ? 16 : killType === 'elite' ? 12 : 8; const ringPositions = []; for (let i = 0; i < ringCount; i++) { const angle = (i / ringCount) * Math.PI * 2; // v7.89: Use pooled offset vector for calculation, then clone for storage this._tempRingOffset.set( Math.cos(angle) * 0.5, 0.3, Math.sin(angle) * 0.5 ); ringPositions.push(pos.clone().add(this._tempRingOffset)); } for (let i = 0; i < ringCount; i++) { const ringPos = ringPositions[i]; setTimeout(() => { if (particles) { particles.emit(ringPos, 2, mobColor, { spread: 2, lifetime: 600, gravity: -0.5 }); } }, i * 15); } } // 3. DEATH LIGHTING FLASH - centered on death position if (typeof CombatLightSystem !== 'undefined' && CombatLightSystem.initialized) { CombatLightSystem.flash( pos, killType === 'boss' ? 0xffd700 : killType === 'elite' ? 0xffaa00 : 0xffffff, cfg.lightIntensity, cfg.burstDuration * 2, cfg.lightRadius ); } // 4. SCREEN COMPRESSION EFFECT (brief FOV change) if (cfg.screenFlash && typeof camera !== 'undefined' && camera.fov) { const originalFOV = camera.fov; const compressionAmount = killType === 'boss' ? 5 : killType === 'elite' ? 3 : 1.5; camera.fov = originalFOV - compressionAmount; camera.updateProjectionMatrix(); setTimeout(() => { camera.fov = originalFOV; camera.updateProjectionMatrix(); }, cfg.burstDuration); } // 5. WHITE FLASH on the mob mesh itself if (mob.traverse) { mob.traverse(child => { if (child.material?.emissive) { const origColor = child.material.emissive.getHex(); const origIntensity = child.material.emissiveIntensity || 0; child.material.emissive.setHex(0xffffff); child.material.emissiveIntensity = cfg.lightIntensity; setTimeout(() => { if (child.material) { child.material.emissive.setHex(origColor); child.material.emissiveIntensity = origIntensity; } }, cfg.burstDuration * 0.5); } }); } // Trigger the dissolution system after the burst setTimeout(() => { if (typeof DeathDissolutionSystem !== 'undefined') { DeathDissolutionSystem.trigger(mob, mobColor); } }, cfg.burstDuration * 0.7); } }; // ============================================ // v7.26: ENVIRONMENTAL MEMORY SCARS SYSTEM (8-Strategy Consensus) // Boss/Elite kills leave permanent visual markers on the terrain // Stored in localStorage, rendered on world load // "The world REMEMBERS your victories" // ============================================ const MemoryScarsSystem = { scars: [], maxScars: 50, // Performance limit loaded: false, // Scar types with visual configs scarTypes: { boss: { color: 0xffd700, size: 4, intensity: 1.5, particles: true, glow: true, name: 'Victory Monument' }, elite: { color: 0xff6600, size: 2.5, intensity: 1.0, particles: false, glow: true, name: 'Battle Scar' }, legendary: { color: 0xbf00ff, size: 5, intensity: 2.0, particles: true, glow: true, name: 'Legendary Conquest' } }, // Initialize - load scars from localStorage // v8.28: Use ErrorRecovery for safer parsing init() { if (this.loaded) return; const saved = ErrorRecovery.safeLocalStorage.get('leviathan_memory_scars'); if (saved) { const parsed = ErrorRecovery.safeJSONParse(saved, { scars: [] }); this.scars = parsed.scars || []; if (DEBUG_LOGGING) console.log(`[MemoryScars] Loaded ${this.scars.length} memory scars`); } this.loaded = true; }, // Save scars to localStorage save() { try { localStorage.setItem('leviathan_memory_scars', JSON.stringify({ scars: this.scars, lastSaved: Date.now() })); } catch (e) { console.warn('[MemoryScars] Failed to save scars:', e); } }, // Create a new memory scar at kill location createScar(position, killType, enemyName, extraData = {}) { if (!position) return null; // Only track elite/boss/legendary kills const scarConfig = this.scarTypes[killType]; if (!scarConfig) return null; const scar = { id: `scar_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, x: position.x, y: position.y || 0, z: position.z, type: killType, enemyName: enemyName, timestamp: Date.now(), planetSeed: gameData.currentPlanet?.seed || 'unknown', galaxyId: gameData.currentGalaxy?.id || 'default', playerLevel: gameData.skills?.combat?.level || 1, ...extraData }; this.scars.push(scar); // Enforce max limit (remove oldest) while (this.scars.length > this.maxScars) { const removed = this.scars.shift(); this.removeScarMesh(removed.id); } this.save(); this.renderScar(scar); // Notification const timeAgo = 'just now'; if (typeof showNotification === 'function') { showNotification(`🏛️ ${scarConfig.name} created: "${enemyName} fell here"`, 'legendary'); } return scar; }, // Render a single scar in the 3D scene renderScar(scar) { if (!scene || !scar) return; const config = this.scarTypes[scar.type]; if (!config) return; // Create scar group const scarGroup = new THREE.Group(); scarGroup.name = `memory_scar_${scar.id}`; scarGroup.userData.scarId = scar.id; // Ground burn mark (dark crater) const craterGeo = new THREE.CircleGeometry(config.size, 32); const craterMat = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 1, metalness: 0, transparent: true, opacity: 0.7, side: THREE.DoubleSide }); const crater = new THREE.Mesh(craterGeo, craterMat); crater.rotation.x = -Math.PI / 2; crater.position.y = 0.02; scarGroup.add(crater); // Glowing ring around crater const ringGeo = new THREE.RingGeometry(config.size * 0.9, config.size * 1.1, 32); const ringMat = new THREE.MeshBasicMaterial({ color: config.color, transparent: true, opacity: 0.6, side: THREE.DoubleSide }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.rotation.x = -Math.PI / 2; ring.position.y = 0.03; scarGroup.add(ring); // Central monument/crystal const monumentGeo = new THREE.ConeGeometry(config.size * 0.3, config.size * 1.5, 6); const monumentMat = new THREE.MeshStandardMaterial({ color: config.color, emissive: config.color, emissiveIntensity: config.intensity * 0.5, metalness: 0.8, roughness: 0.2, transparent: true, opacity: 0.8 }); const monument = new THREE.Mesh(monumentGeo, monumentMat); monument.position.y = config.size * 0.75; scarGroup.add(monument); // Ghostly afterimage particles (floating embers) // v7.85: Use shared geometry pool to avoid 8 geometry allocations per scar if (config.particles) { const emberCount = 8; for (let i = 0; i < emberCount; i++) { const emberMat = new THREE.MeshBasicMaterial({ color: config.color, transparent: true, opacity: 0.6 }); const ember = new THREE.Mesh(_effectGeometryPool.emberLarge, emberMat); const angle = (i / emberCount) * Math.PI * 2; const radius = config.size * 0.6; ember.position.set( Math.cos(angle) * radius, 0.5 + Math.random() * 1.5, Math.sin(angle) * radius ); ember.userData.floatOffset = Math.random() * Math.PI * 2; ember.userData.floatSpeed = 0.5 + Math.random() * 0.5; scarGroup.add(ember); } } // Point light for glow effect if (config.glow) { const light = new THREE.PointLight(config.color, config.intensity * 0.3, config.size * 3); light.position.y = 1; scarGroup.add(light); } // Position the scar scarGroup.position.set(scar.x, scar.y, scar.z); // Store reference for animation scarGroup.userData.config = config; scarGroup.userData.scar = scar; scene.add(scarGroup); // Add to tracking if (!this.renderedScars) this.renderedScars = new Map(); this.renderedScars.set(scar.id, scarGroup); }, // Remove a scar mesh from scene removeScarMesh(scarId) { if (!this.renderedScars) return; const mesh = this.renderedScars.get(scarId); if (mesh && scene) { scene.remove(mesh); this.renderedScars.delete(scarId); } }, // Render all scars for current planet // v8.24: Use for loops instead of forEach for consistency renderScarsForPlanet(planetSeed) { if (!this.loaded) this.init(); // Clear existing rendered scars if (this.renderedScars) { for (const mesh of this.renderedScars.values()) { if (scene) scene.remove(mesh); } this.renderedScars.clear(); } // Render scars matching this planet const planetScars = this.scars.filter(s => s.planetSeed === planetSeed); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[MemoryScars] Rendering ${planetScars.length} scars for planet ${planetSeed}`); for (let i = 0; i < planetScars.length; i++) { this.renderScar(planetScars[i]); } }, // Animate floating embers // v8.22: forEach-to-for loop optimization update(dt) { if (!this.renderedScars) return; const time = performance.now() * 0.001; for (const group of this.renderedScars.values()) { const children = group.children; for (let i = 0, len = children.length; i < len; i++) { const child = children[i]; if (child.userData.floatOffset !== undefined) { // Float up and down const baseY = 0.5 + Math.random() * 1.5; child.position.y = baseY + Math.sin(time * child.userData.floatSpeed + child.userData.floatOffset) * 0.3; } // Slowly rotate monument if (child.geometry?.type === 'ConeGeometry') { child.rotation.y += dt * 0.2; } } } }, // Get scar info for tooltip/interaction getScarInfo(scarId) { const scar = this.scars.find(s => s.id === scarId); if (!scar) return null; const config = this.scarTypes[scar.type]; const timeAgo = this.formatTimeAgo(scar.timestamp); return { title: config.name, description: `"${scar.enemyName} fell here"`, timeAgo: timeAgo, playerLevel: scar.playerLevel }; }, // Format timestamp as "X days ago" formatTimeAgo(timestamp) { const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return 'just now'; if (seconds < 3600) return `${Math.floor(seconds / 60)} minutes ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours ago`; return `${Math.floor(seconds / 86400)} days ago`; }, // Clear all scars (reset) // v8.24: Use for-of instead of forEach for consistency clearAll() { this.scars = []; if (this.renderedScars) { for (const mesh of this.renderedScars.values()) { if (scene) scene.remove(mesh); } this.renderedScars.clear(); } this.save(); console.log('[MemoryScars] All scars cleared'); } }; // ============================================ // v7.24: MOB SPAWN MATERIALIZATION SYSTEM // Animated spawn effect - inverse of death dissolve // Scale up from 0, fade in, convergent particles // ============================================ const MobSpawnSystem = { spawning: [], maxSpawning: 5, trigger(mob, mobColor) { if (!mob || !scene) return; if (this.spawning.length >= this.maxSpawning) { // Performance fallback: instant spawn mob.visible = true; return; } if (mob.userData?.isSpawning) return; // Start invisible and scaled down mob.scale.setScalar(0.1); mob.visible = true; // Set materials to transparent for fade-in mob.traverse(child => { if (child.material) { child.material.transparent = true; child.material.opacity = 0; } }); // Hide HP bar during spawn animation if (mob.userData.hpBar) { mob.userData.hpBar.visible = false; } mob.userData.isSpawning = true; mob.userData.spawnData = { startTime: performance.now(), duration: 400, // Faster than death (spawn should feel snappy) originalColor: mobColor || 0x44ff44, originalY: mob.position.y, phase: 0 }; this.spawning.push(mob); // Convergent particles (spiral inward toward mob) // v7.97: Pre-compute particle positions to avoid clone()+new Vector3() in setTimeout closures const particleCount = 8; const spawnParticlePositions = []; for (let i = 0; i < particleCount; i++) { const angle = (i / particleCount) * Math.PI * 2; // Snapshot position + offset now (mob.position may change by setTimeout time) spawnParticlePositions.push({ x: mob.position.x + Math.cos(angle) * 2.5, y: mob.position.y + Math.random() * 1.5, z: mob.position.z + Math.sin(angle) * 2.5 }); } // v7.97: Single temp vector reused across all setTimeout calls const _spawnEmitPos = GlobalVec3Pool.acquire(); for (let i = 0; i < particleCount; i++) { if (particles) { const precomputed = spawnParticlePositions[i]; setTimeout(() => { if (particles) { _spawnEmitPos.set(precomputed.x, precomputed.y, precomputed.z); particles.emit( _spawnEmitPos, 2, mobColor || 0x44ff44, { spread: 0.3, lifetime: 300, gravity: 0 } ); // Release on last particle if (i === particleCount - 1) { GlobalVec3Pool.release(_spawnEmitPos); } } }, i * 30); } } // Spawn light flash (green tint) if (CombatLightSystem.initialized) { CombatLightSystem.flash(mob.position, 0x44ff44, 1.5, 250, 6); } }, update(dt) { const now = performance.now(); this.spawning = this.spawning.filter(mob => { if (!mob || !mob.parent) return false; const data = mob.userData?.spawnData; if (!data) return false; const elapsed = now - data.startTime; const t = Math.min(1, elapsed / data.duration); // Phase 1 (0-30%): Rapid scale-up with glow if (t < 0.3) { const scaleT = t / 0.3; // Elastic overshoot for "pop" feel const scale = scaleT * 1.15; mob.scale.setScalar(Math.max(0.1, scale)); // Bright emissive during materialization mob.traverse(child => { if (child.material?.emissive) { child.material.emissive.setHex(0xffffff); child.material.emissiveIntensity = 2 * (1 - scaleT); } if (child.material) { child.material.opacity = scaleT; } }); } // Phase 2 (30-85%): Settle to final scale, fade glow else if (t < 0.85) { const settleT = (t - 0.3) / 0.55; // Ease back from 1.15 to 1.0 (elastic settle) const scale = 1.15 - (settleT * 0.15); mob.scale.setScalar(scale); mob.traverse(child => { if (child.material?.emissive) { child.material.emissiveIntensity = 0.5 * (1 - settleT); } if (child.material) { child.material.opacity = 1; } }); } // Phase 3 (85-100%): Final snap to normal + completion else { mob.scale.setScalar(1); mob.traverse(child => { if (child.material?.emissive) { // Reset to original emissive const originalEmissive = mob.userData.isElite ? 0x440044 : 0x003300; child.material.emissive.setHex(originalEmissive); child.material.emissiveIntensity = mob.userData.isElite ? 0.5 : 0.2; } if (child.material) { child.material.opacity = 1; child.material.transparent = false; } }); // Show HP bar now that spawn is complete if (mob.userData.hpBar) { mob.userData.hpBar.visible = true; } mob.userData.isSpawning = false; delete mob.userData.spawnData; if (t >= 1) { return false; // Remove from tracking } } return true; }); } }; // v4.4: Enhanced Hit Flash // v7.28: Enhanced enemy hit flash (8-Strategy Consensus Cycle 1 - Game Feel + Combat + Visual) // v7.31: Boosted emissive flash intensity (8-Strategy Consensus Cycle 4 - 4/8 agents) // White flash confirms damage instantly at point of impact function flashTargetHit(target, flashColor = 0xffffff, options = {}) { const { duration = 60, intensity = 1.2, scaleFlash = true, emissiveBoost = 2.5 } = options; const originalMaterials = []; target.traverse(child => { if (child.material && child.material.color) { originalMaterials.push({ mesh: child, color: child.material.color.getHex(), emissive: child.material.emissive?.getHex() || 0, emissiveIntensity: child.material.emissiveIntensity || 0 }); // v7.28: Always flash white first for instant damage confirmation child.material.color.setHex(0xffffff); if (child.material.emissive) { child.material.emissive.setHex(0xffffff); // v7.31: Use emissiveBoost for more visible hit feedback child.material.emissiveIntensity = emissiveBoost; } } }); // v7.28: Brief scale punch for extra "weight" feedback if (scaleFlash && target.scale) { const origScale = target.scale.x; target.scale.setScalar(origScale * 0.85); setTimeout(() => { if (target.scale) target.scale.setScalar(origScale * 1.05); }, duration * 0.4); setTimeout(() => { if (target.scale) target.scale.setScalar(origScale); }, duration); } // v7.28: Two-phase flash: white (confirmation) -> color (damage type) -> original setTimeout(() => { // Phase 2: Transition to damage color originalMaterials.forEach(data => { if (data.mesh.material) { data.mesh.material.color.setHex(flashColor); if (data.mesh.material.emissive) { data.mesh.material.emissive.setHex(flashColor); data.mesh.material.emissiveIntensity = intensity * 0.7; } } }); }, duration * 0.4); setTimeout(() => { // Phase 3: Restore original originalMaterials.forEach(data => { if (data.mesh.material) { data.mesh.material.color.setHex(data.color); if (data.mesh.material.emissive) { data.mesh.material.emissive.setHex(data.emissive); data.mesh.material.emissiveIntensity = data.emissiveIntensity; } } }); }, duration); } // ============================================ // v8.0: IMPACT SQUASH-STRETCH ANIMATION - 8-Agent Consensus Cycle 7 // Classic animation principle for weighty, satisfying hit feedback // ============================================ const SQUASH_STRETCH_CONFIG = { ENABLED: true, SQUASH_DURATION: 40, STRETCH_DURATION: 60, SETTLE_DURATION: 100, BASE_INTENSITY: 0.15, DAMAGE_SCALING: 0.005, COMBO_SCALING: 0.02, MAX_INTENSITY: 0.4, CRIT_MULTIPLIER: 1.5, FINISHER_MULTIPLIER: 2.0 }; let squashStretchStates = new Map(); function applySquashStretch(target, damage = 5, comboCount = 0, isCrit = false, isFinisher = false) { if (!SQUASH_STRETCH_CONFIG.ENABLED || !target?.scale) return; let intensity = SQUASH_STRETCH_CONFIG.BASE_INTENSITY + (damage * SQUASH_STRETCH_CONFIG.DAMAGE_SCALING) + (comboCount * SQUASH_STRETCH_CONFIG.COMBO_SCALING); if (isFinisher) intensity *= SQUASH_STRETCH_CONFIG.FINISHER_MULTIPLIER; else if (isCrit) intensity *= SQUASH_STRETCH_CONFIG.CRIT_MULTIPLIER; intensity = Math.min(intensity, SQUASH_STRETCH_CONFIG.MAX_INTENSITY); // v7.95: Use GlobalVec3Pool.temp() to avoid clone() allocation let hitAxisX = 0.5, hitAxisZ = 0.5; if (worldState?.player?.position && target.position) { const toEnemy = GlobalVec3Pool.temp().copy(target.position).sub(worldState.player.position).normalize(); hitAxisX = Math.abs(toEnemy.x); hitAxisZ = Math.abs(toEnemy.z); } if (squashStretchStates.has(target.uuid)) { squashStretchStates.delete(target.uuid); } // v7.95: Store scale as primitives to avoid needing a persistent Vector3 allocation const originalScaleX = target.scale.x; const originalScaleY = target.scale.y; const originalScaleZ = target.scale.z; const startTime = performance.now(); squashStretchStates.set(target.uuid, { originalScale: { x: originalScaleX, y: originalScaleY, z: originalScaleZ }, intensity, hitAxisX, hitAxisZ, startTime }); const animate = () => { // v8.34: Skip animation when tab is hidden if (!isPageVisible) { requestAnimationFrame(animate); return; } if (!target.parent || !squashStretchStates.has(target.uuid)) { squashStretchStates.delete(target.uuid); return; } const state = squashStretchStates.get(target.uuid); const elapsed = performance.now() - state.startTime; const { SQUASH_DURATION, STRETCH_DURATION, SETTLE_DURATION } = SQUASH_STRETCH_CONFIG; const total = SQUASH_DURATION + STRETCH_DURATION + SETTLE_DURATION; if (elapsed >= total) { // v7.95: Use set() since originalScale is now a plain object target.scale.set(state.originalScale.x, state.originalScale.y, state.originalScale.z); squashStretchStates.delete(target.uuid); return; } let sx = 1, sy = 1, sz = 1; if (elapsed < SQUASH_DURATION) { const t = elapsed / SQUASH_DURATION; const sq = state.intensity * Math.sin(t * Math.PI / 2); sx = 1 - sq * state.hitAxisX + sq * 0.3 * (1 - state.hitAxisX); sy = 1 + sq * 0.5; sz = 1 - sq * state.hitAxisZ + sq * 0.3 * (1 - state.hitAxisZ); } else if (elapsed < SQUASH_DURATION + STRETCH_DURATION) { const t = (elapsed - SQUASH_DURATION) / STRETCH_DURATION; const st = state.intensity * 0.5 * Math.sin(t * Math.PI); sx = 1 + st * state.hitAxisX * 0.3; sy = 1 - st * 0.3; sz = 1 + st * state.hitAxisZ * 0.3; } else { const t = (elapsed - SQUASH_DURATION - STRETCH_DURATION) / SETTLE_DURATION; const ease = 1 - t; const se = state.intensity * 0.1 * ease; sx = 1 + se * 0.1 * Math.sin(t * Math.PI * 2); sz = 1 + se * 0.1 * Math.sin(t * Math.PI * 2); } target.scale.set(state.originalScale.x * sx, state.originalScale.y * sy, state.originalScale.z * sz); requestAnimationFrame(animate); }; requestAnimationFrame(animate); } // ============================================ // v7.24: HIT STAGGER/RECOIL SYSTEM - 8-Strategy Consensus Cycle 9 // Adds visible recoil animation on NON-LETHAL hits to make every hit feel impactful // ============================================ const HitStaggerSystem = { config: { enabled: true, baseDuration: 150, // ms baseRecoilDistance: 0.3, // world units damageScaling: 0.008, // extra recoil per damage maxRecoilDistance: 0.8, // max recoil rotationAmount: 0.15, // radians of rotation wobble returnEasing: 'easeOutElastic' }, activeStates: new Map(), maxConcurrent: 20, // v7.89: Pre-allocated vectors for recoil calculations _tempRecoilDir: null, _tempOriginalPos: null, _tempRecoilTarget: null, trigger(target, damage, attackDirection, options = {}) { if (!this.config.enabled || !target?.position) return; if (this.activeStates.size >= this.maxConcurrent) return; const { isCrit = false, isFinisher = false, willDie = false } = options; // Don't stagger on killing blows - death effects handle that if (willDie) return; // v7.89: Lazy-init pooled vectors if (!this._tempRecoilDir) this._tempRecoilDir = new THREE.Vector3(); if (!this._tempOriginalPos) this._tempOriginalPos = new THREE.Vector3(); if (!this._tempRecoilTarget) this._tempRecoilTarget = new THREE.Vector3(); // Calculate recoil intensity const baseRecoil = this.config.baseRecoilDistance; const damageRecoil = Math.min(damage * this.config.damageScaling, this.config.maxRecoilDistance - baseRecoil); let recoilDistance = baseRecoil + damageRecoil; if (isCrit) recoilDistance *= 1.3; if (isFinisher) recoilDistance *= 1.6; // v7.89: Use pooled vector for recoil direction instead of clone() if (attackDirection) { this._tempRecoilDir.copy(attackDirection).normalize(); } else if (worldState?.player && target.position) { this._tempRecoilDir.copy(target.position).sub(worldState.player.position).normalize(); } else { this._tempRecoilDir.set(Math.random() - 0.5, 0, Math.random() - 0.5).normalize(); } this._tempRecoilDir.y = 0; // Keep it horizontal // v7.95: Optimized - calculate recoil target without intermediate clone() // Acquire vectors from pool for state storage (must persist across animation frames) const originalPosition = GlobalVec3Pool.acquire().copy(target.position); const recoilTarget = GlobalVec3Pool.acquire().copy(target.position); // Add scaled recoil direction directly without clone() recoilTarget.x += this._tempRecoilDir.x * recoilDistance; recoilTarget.y += this._tempRecoilDir.y * recoilDistance; recoilTarget.z += this._tempRecoilDir.z * recoilDistance; const state = { startTime: performance.now(), duration: this.config.baseDuration * (isFinisher ? 1.5 : 1), originalPosition: originalPosition, recoilTarget: recoilTarget, originalRotationY: target.rotation?.y || 0, rotationWobble: (Math.random() - 0.5) * this.config.rotationAmount * (isCrit ? 2 : 1) }; this.activeStates.set(target.uuid, state); this.animateStagger(target, state); }, animateStagger(target, state) { if (!target || !this.activeStates.has(target.uuid)) return; const elapsed = performance.now() - state.startTime; const progress = Math.min(elapsed / state.duration, 1); // Elastic ease-out for snappy recoil then bounce back const eased = this.easeOutElastic(progress); // Phase 1 (0-0.3): Quick recoil backward // Phase 2 (0.3-1.0): Elastic return to original position if (progress < 0.3) { const recoilProgress = progress / 0.3; const easeOut = 1 - Math.pow(1 - recoilProgress, 3); target.position.lerpVectors(state.originalPosition, state.recoilTarget, easeOut); if (target.rotation) { target.rotation.y = state.originalRotationY + state.rotationWobble * easeOut; } } else { const returnProgress = (progress - 0.3) / 0.7; const returnEased = this.easeOutElastic(returnProgress); target.position.lerpVectors(state.recoilTarget, state.originalPosition, returnEased); if (target.rotation) { target.rotation.y = state.originalRotationY + state.rotationWobble * (1 - returnEased); } } if (progress < 1) { requestAnimationFrame(() => this.animateStagger(target, state)); } else { // Ensure final position is exact target.position.copy(state.originalPosition); if (target.rotation) target.rotation.y = state.originalRotationY; // v7.95: Release pooled vectors back to pool GlobalVec3Pool.releaseAll(state.originalPosition, state.recoilTarget); this.activeStates.delete(target.uuid); } }, easeOutElastic(t) { if (t === 0 || t === 1) return t; return Math.pow(2, -10 * t) * Math.sin((t - 0.1) * 5 * Math.PI) + 1; }, // Emit small impact particles at hit location emitHitParticles(position, damage, color = 0xffffff) { if (!particles) return; const count = Math.min(5 + Math.floor(damage / 5), 15); particles.emit(position, count, color, { spread: 1.5, lifetime: 300, velocity: { x: 0, y: 2, z: 0 } }); } }; // v4.4: Environmental Particle System // v7.31: Object pooling for environment particles (8-Strategy Consensus Cycle 4 - 3/8 agents) // Eliminates per-frame allocations for smooth performance class EnvironmentParticles { constructor() { this.activeParticles = []; this.particlePool = []; // v7.31: Object pool for reuse this.maxPoolSize = 80; // v7.31: Maximum pooled particles this.sharedGeometry = new THREE.SphereGeometry(0.08, 4, 4); // v7.31: Shared geometry this.materialCache = {}; // v7.31: Cache materials by color this._tempVelocity = new THREE.Vector3(); // v7.32: Pre-allocated temp vector (Cycle 5 Consensus) this.currentBiome = null; this.biomeConfigs = { Terra: { color: 0x88aa44, count: 20, speed: 1.5, type: 'leaves', gravity: 2 }, Desert: { color: 0xddcc99, count: 30, speed: 3, type: 'dust', gravity: 0.5 }, Ice: { color: 0xeeffff, count: 40, speed: 0.8, type: 'snow', gravity: 1 }, Volcanic: { color: 0xff4400, count: 25, speed: 4, type: 'embers', gravity: -3 }, Alien: { color: 0xff00ff, count: 20, speed: 1, type: 'spores', gravity: -0.5 }, // v7.24: FACTORY INDUSTRIAL PARTICLES (8-Strategy Consensus) Factory: { color: 0x556677, count: 35, speed: 0.6, type: 'smoke', gravity: -1.2 } }; // v7.31: Pre-warm the pool with common biome colors this._prewarmPool(); } // v7.31: Pre-create particles to avoid runtime allocations _prewarmPool() { const commonColors = [0x88aa44, 0xddcc99, 0xeeffff, 0xff4400, 0xff00ff, 0x556677]; commonColors.forEach(color => { for (let i = 0; i < 8; i++) { const particle = this._createParticle(color); particle.mesh.visible = false; this.particlePool.push(particle); } }); } // v7.31: Get or create cached material _getMaterial(color) { if (!this.materialCache[color]) { this.materialCache[color] = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.6 }); } return this.materialCache[color]; } // v7.31: Create a new particle object _createParticle(color) { return { mesh: new THREE.Mesh(this.sharedGeometry, this._getMaterial(color)), velocity: new THREE.Vector3(), life: 0, baseOpacity: 0.6 }; } // v7.31: Get particle from pool or create new _acquireParticle(color, config) { let particle = this.particlePool.pop(); if (particle) { // Reuse pooled particle, update material if needed if (particle.mesh.material.color.getHex() !== color) { particle.mesh.material = this._getMaterial(color); } } else { // Create new particle if pool empty particle = this._createParticle(color); scene.add(particle.mesh); } particle.mesh.visible = true; particle.mesh.material.opacity = 0.6; particle.baseOpacity = 0.6; return particle; } // v7.31: Return particle to pool _releaseParticle(particle) { particle.mesh.visible = false; if (this.particlePool.length < this.maxPoolSize) { this.particlePool.push(particle); } else { // Pool full, actually dispose scene.remove(particle.mesh); } } startBiome(biome) { if (this.currentBiome === biome) return; this.stop(); this.currentBiome = biome; // v8.0: Track exploration for behavioral commentary if (typeof trackBehaviorPattern === 'function') { trackBehaviorPattern('explore'); } } stop() { // v7.31: Return all active particles to pool instead of disposing // v8.21: Use for loop instead of forEach for (let i = 0; i < this.activeParticles.length; i++) { this._releaseParticle(this.activeParticles[i]); } this.activeParticles = []; this.currentBiome = null; } update(dt, playerPos) { if (!this.currentBiome) return; const config = this.biomeConfigs[this.currentBiome]; if (!config) return; // Spawn new particles near player using pool while (this.activeParticles.length < config.count) { const angle = Math.random() * Math.PI * 2; const dist = 5 + Math.random() * 20; // v7.31: Acquire from pool instead of creating new const particle = this._acquireParticle(config.color, config); particle.velocity.set( (Math.random() - 0.5) * config.speed, config.gravity > 0 ? -Math.abs(config.gravity) : config.gravity, (Math.random() - 0.5) * config.speed ); particle.life = 5 + Math.random() * 5; particle.mesh.position.set( playerPos.x + Math.cos(angle) * dist, playerPos.y + 5 + Math.random() * 10, playerPos.z + Math.sin(angle) * dist ); this.activeParticles.push(particle); } // Update particles - v7.31: Release to pool instead of disposing const stillActive = []; for (let i = 0; i < this.activeParticles.length; i++) { const p = this.activeParticles[i]; p.life -= dt; if (p.life <= 0 || p.mesh.position.y < 0) { this._releaseParticle(p); continue; } // v7.32: Use pre-allocated temp vector to avoid GC (Cycle 5 Consensus - 4/8 agents) this._tempVelocity.copy(p.velocity).multiplyScalar(dt); p.mesh.position.add(this._tempVelocity); // Sway for leaves/snow if (config.type === 'leaves' || config.type === 'snow') { p.mesh.position.x += Math.sin(performance.now() * 0.002 + p.life) * 0.02; } // Pulse for spores if (config.type === 'spores') { p.mesh.material.opacity = 0.3 + Math.sin(performance.now() * 0.005) * 0.3; } stillActive.push(p); } this.activeParticles = stillActive; } // v7.31: Cleanup method for full disposal // v8.21: Use for loops instead of forEach dispose() { this.stop(); for (let i = 0; i < this.particlePool.length; i++) { scene.remove(this.particlePool[i].mesh); } this.particlePool = []; this.sharedGeometry.dispose(); const materials = Object.values(this.materialCache); for (let i = 0; i < materials.length; i++) { materials[i].dispose(); } this.materialCache = {}; } } let envParticles; // v4.5: Player Dodge System const DODGE_CONFIG = { DISTANCE: 6, DURATION: 180, // ms COOLDOWN: 600, // ms IFRAMES: 150 // invincibility duration in ms }; let dodgeState = { active: false, direction: new THREE.Vector3(), startTime: 0, cooldownEnd: 0, iframesEnd: 0, _tempMoveVec: new THREE.Vector3() // v7.84: Pre-allocated for updateDodge velocity calc }; // v12.26: SPAWN INVINCIBILITY SYSTEM - Prevents respawn death loops const SPAWN_PROTECTION = { DURATION: 3000, // 3 seconds of invincibility after respawn endTime: 0 // When spawn protection expires }; // v4.6: Parry/Counter System const PARRY_CONFIG = { WINDOW: 250, // ms before attack lands to trigger parry STUN_DURATION: 1500, // ms enemy is stunned CRIT_MULTIPLIER: 2.5, // damage multiplier during crit window CRIT_WINDOW: 2000 // ms player has to land crits }; let parryState = { critWindowEnd: 0, lastParryTime: 0 }; // v4.8: Combo Attack System // v7.2: Enhanced with Perfect Timing Window (8-Strategy Consensus Round 1) const COMBO_CONFIG = { WINDOW: 1200, // ms to chain next hit MAX_HITS: 5, // maximum combo length DAMAGE_MULT: [1.0, 1.15, 1.35, 1.6, 2.0], // damage multiplier per hit FINISHER_BONUS: 1.5, // extra multiplier on max combo hit BREAK_ON_DAMAGE: true, // combo breaks if player takes damage // v7.2: Perfect Timing Window (8-Strategy Consensus) PERFECT_WINDOW: { START: 180, // Perfect timing starts 180ms after last hit END: 400, // Perfect timing ends at 400ms EXTENSION: 350, // Extends next window by 350ms DAMAGE_BONUS: 0.20, // +20% damage for perfect timing MAX_EXTENDED_WINDOW: 2000 // Maximum extended window size } }; let comboState = { count: 0, lastHitTime: 0, active: false, // v7.2: Perfect Chain tracking (8-Strategy Consensus) currentWindow: COMBO_CONFIG.WINDOW, perfectChain: 0, isPerfect: false }; function updateCombo(hitTime) { const timeSinceLastHit = hitTime - comboState.lastHitTime; const perfectWindow = COMBO_CONFIG.PERFECT_WINDOW; // v7.2: Use dynamic window (can be extended by perfect chains) if (comboState.active && timeSinceLastHit <= comboState.currentWindow) { // Check for perfect timing (8-Strategy Consensus) if (timeSinceLastHit >= perfectWindow.START && timeSinceLastHit <= perfectWindow.END) { // PERFECT CHAIN! comboState.isPerfect = true; comboState.perfectChain++; // Extend the NEXT window comboState.currentWindow = Math.min( COMBO_CONFIG.WINDOW + perfectWindow.EXTENSION, perfectWindow.MAX_EXTENDED_WINDOW ); // Visual/Audio feedback for perfect timing showPerfectChainFeedback(comboState.perfectChain); triggerHitStop(HIT_STOP_LIGHT + 25); // Slightly longer hit-stop for emphasis } else { // Good timing, but not perfect - reset window to base comboState.isPerfect = false; comboState.currentWindow = COMBO_CONFIG.WINDOW; // Perfect chain resets if hit too late if (timeSinceLastHit > perfectWindow.END) { comboState.perfectChain = 0; } } // Continue combo comboState.count = Math.min(comboState.count + 1, COMBO_CONFIG.MAX_HITS - 1); } else { // Start new combo or combo broken comboState.count = 0; comboState.active = true; comboState.currentWindow = COMBO_CONFIG.WINDOW; comboState.perfectChain = 0; comboState.isPerfect = false; } comboState.lastHitTime = hitTime; // v6.43: Update combo UI display updateComboUI(); // v8.0: Apply Combo Cooldown Reduction (8-Agent Consensus Cycle 8) if (typeof applyComboCoolddownReduction === 'function') { const isFinisher = comboState.count >= COMBO_CONFIG.MAX_HITS - 1; applyComboCoolddownReduction(comboState.count, comboState.isPerfect, isFinisher); } return comboState.count; } // v7.2: Perfect Chain Visual Feedback (8-Strategy Consensus Round 1) function showPerfectChainFeedback(chainCount) { const cache = getComboUICache(); if (cache.count) { // Golden flash on combo counter for perfect cache.count.style.color = '#ffd700'; cache.count.style.textShadow = '0 0 20px #ffd700, 0 0 40px #ff8800'; setTimeout(() => { if (cache.count) cache.count.style.textShadow = ''; }, 300); } // Spawn "PERFECT!" floater if (worldState.player) { if (chainCount === 1) { spawnFloater(worldState.player.position, 'PERFECT!', '#ffd700'); } else if (chainCount >= 2) { spawnFloater(worldState.player.position, `PERFECT x${chainCount}!`, '#ffd700'); } } // Screen flash for perfect timing const border = document.getElementById('impact-border'); if (border) { border.style.boxShadow = 'inset 0 0 80px rgba(255, 215, 0, 0.4)'; setTimeout(() => border.style.boxShadow = 'none', 200); } // Ascending pitch audio if (AudioSystem?.penta) { const pitchIndex = Math.min(chainCount, 5); const notes = [AudioSystem.penta.C4, AudioSystem.penta.E4, AudioSystem.penta.G4, AudioSystem.penta.C5, AudioSystem.penta.E5]; AudioSystem.playGentle(notes[pitchIndex - 1] || notes[0], 0.1, 0.15); } // v7.69: PERFECT CHAIN HAPTIC ESCALATION (8-Agent Consensus Cycle 44) // Escalating vibration intensity for consecutive perfect timing hits if (typeof MobileHaptics !== 'undefined') { if (chainCount === 1) { MobileHaptics.vibrate('parry'); // [20, 10, 50] - satisfying but not overwhelming } else if (chainCount === 2) { MobileHaptics.vibrateCustom([30, 15, 60, 15, 30]); // Double pulse escalation } else if (chainCount === 3) { MobileHaptics.vibrateCustom([40, 20, 70, 20, 70, 20, 40]); // Triple pulse } else if (chainCount >= 4) { MobileHaptics.vibrateCustom([50, 25, 80, 25, 80, 25, 100]); // Maximum celebration } } // v7.70: Time Dilation on perfect parries (8-Agent Consensus Cycle 45) if (typeof TimeDilationSystem !== 'undefined' && chainCount >= 2) { TimeDilationSystem.trigger('perfectParry'); } } function getComboMultiplier() { if (!comboState.active) return 1.0; // v8.29: Add boundary check for combo count const safeCount = Math.max(0, Math.min(comboState.count, COMBO_CONFIG.MAX_HITS - 1)); let mult = COMBO_CONFIG.DAMAGE_MULT[safeCount] || COMBO_CONFIG.DAMAGE_MULT[COMBO_CONFIG.MAX_HITS - 1]; // Finisher bonus at max combo if (comboState.count >= COMBO_CONFIG.MAX_HITS - 1) { mult *= COMBO_CONFIG.FINISHER_BONUS; } // v7.2: Perfect Chain damage bonus (8-Strategy Consensus Round 1) if (comboState.isPerfect) { mult *= (1 + COMBO_CONFIG.PERFECT_WINDOW.DAMAGE_BONUS); } // Consecutive perfects add escalating bonus if (comboState.perfectChain >= 3) { mult *= 1 + (0.05 * (comboState.perfectChain - 2)); // +5% per perfect after 2nd } // v8.29: Cap multiplier at a reasonable maximum (10x) return Math.min(mult, 10); } // v6.43: Update combo counter UI display // v6.82: Cached DOM references for combo UI let comboDisplayTimeout = null; let _comboUICache = null; function getComboUICache() { if (!_comboUICache) { _comboUICache = { counter: document.getElementById('combo-counter'), count: document.getElementById('combo-count'), mult: document.getElementById('combo-multiplier') }; } return _comboUICache; } function updateComboUI() { const cache = getComboUICache(); if (!cache.counter || !cache.count || !cache.mult) return; const counter = cache.counter; const countEl = cache.count; const multEl = cache.mult; if (comboState.active && comboState.count > 0) { counter.style.display = 'block'; countEl.textContent = comboState.count + 1; // Color escalation based on combo const colors = ['#888', '#ff8800', '#ff4400', '#ff00ff', '#ffd700']; const colorIndex = Math.min(Math.floor(comboState.count / 2), colors.length - 1); countEl.style.color = colors[colorIndex]; // Scale pop effect countEl.style.transform = 'scale(1.3)'; setTimeout(() => countEl.style.transform = 'scale(1)', 100); // Show multiplier const mult = getComboMultiplier(); multEl.textContent = `×${mult.toFixed(1)}`; // Reset hide timer clearTimeout(comboDisplayTimeout); comboDisplayTimeout = setTimeout(() => { counter.style.display = 'none'; }, COMBO_CONFIG.WINDOW + 200); } else { counter.style.display = 'none'; } } function breakCombo() { if (comboState.active) { // v6.35: Trigger combo crescendo resolution before breaking if (typeof comboCrescendo !== 'undefined') { comboCrescendo.comboBreak(comboState.count); } // v6.35: Reset chromatic aura when combo breaks if (typeof comboChromaticSystem !== 'undefined') { comboChromaticSystem.resetAura(); } // v8.0: Reset CDR state when combo ends (8-Agent Consensus Cycle 8) if (typeof resetComboCDRState === 'function') { resetComboCDRState(); } comboState.active = false; comboState.count = 0; updateComboUI(); // v6.43: Hide combo UI when broken } } // v6.12: Victory Streak System (Renamed from Kill Streak for family-friendly gameplay) // v8.0: Enhanced with audio stingers and extended milestones (8-Agent Consensus) const VICTORY_STREAK_CONFIG = { WINDOW: 5000, // ms between victories to maintain streak XP_MULTIPLIERS: [1.0, 1.1, 1.2, 1.3, 1.5, 1.7, 2.0, 2.5, 3.0], // per streak level MILESTONES: { 5: { name: 'Victory Spree!', color: '#ff8800', notes: 2 }, 10: { name: 'On Fire!', color: '#ff4400', notes: 3 }, 15: { name: 'Unstoppable!', color: '#ff0088', notes: 4 }, 20: { name: 'LEGENDARY!', color: '#ffd700', notes: 5 }, 25: { name: '💀 GODLIKE! 💀', color: '#ff00ff', notes: 6 }, 50: { name: '⚡ COSMIC TERROR ⚡', color: '#00ffff', notes: 8 } } }; // Backwards compatibility alias const KILL_STREAK_CONFIG = VICTORY_STREAK_CONFIG; // v8.0: Musical stinger for streak milestones (8-Agent Consensus) function playStreakStinger(noteCount) { if (!AudioSystem || !AudioSystem.ctx) return; try { const ctx = AudioSystem.ctx; const now = ctx.currentTime; const penta = [261.63, 329.63, 392.00, 523.25, 659.25, 783.99, 1046.50, 1318.51]; // C major pentatonic for (let i = 0; i < Math.min(noteCount, penta.length); i++) { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'triangle'; osc.frequency.value = penta[i]; const startTime = now + (i * 0.08); // Rapid ascending notes const duration = 0.15 + (i * 0.02); const volume = 0.12 - (i * 0.01); gain.gain.setValueAtTime(0, startTime); gain.gain.linearRampToValueAtTime(Math.max(0.02, volume), startTime + 0.02); gain.gain.exponentialRampToValueAtTime(0.001, startTime + duration); osc.start(startTime); osc.stop(startTime + duration + 0.1); } } catch (e) { // Silently fail if audio context unavailable } } // ============================================ // v8.0: CLUTCH SURVIVAL CELEBRATION - 8-Agent Consensus // Celebrate when player defeats enemies at death's door or survives close calls! // ============================================ const CLUTCH_CONFIG = { HP_THRESHOLD: 15, // Below this HP = clutch territory CRITICAL_HP: 5, // Extremely clutch COOLDOWN: 10000, // 10 seconds between clutch celebrations lastClutchTime: 0 }; const CLUTCH_ECHO_MESSAGES = [ "THAT WAS INSANE! You were one hit from death!", "Commander... my core nearly stopped! How did you DO that?!", "Death was RIGHT THERE and you SPAT in its face!", "I thought I'd lost you! That was incredible!", "My sensors couldn't believe it - clutch victory!", "Living on the edge... YOU ABSOLUTE LEGEND!", "Heart-stopping moment! You're a survivor, Commander.", "That was poetry in motion at death's door!" ]; function triggerClutchCelebration(playerHP, isKill = true) { const now = performance.now(); if (now - CLUTCH_CONFIG.lastClutchTime < CLUTCH_CONFIG.COOLDOWN) return; if (playerHP > CLUTCH_CONFIG.HP_THRESHOLD) return; if (!worldState.player) return; CLUTCH_CONFIG.lastClutchTime = now; // Determine clutch intensity based on how close to death const isCritical = playerHP <= CLUTCH_CONFIG.CRITICAL_HP; const intensity = isCritical ? 'critical' : 'clutch'; // 1. Brief time slow effect (200ms) if (typeof triggerHitStop === 'function') { triggerHitStop(isCritical ? 150 : 100); } // 2. Screen shake if (typeof screenShake === 'function') { screenShake(isCritical ? 0.6 : 0.4); } // 3. Dramatic visual feedback const clutchText = isCritical ? '💀 CLUTCH! 💀' : '⚡ CLUTCH! ⚡'; const clutchColor = isCritical ? '#ff0000' : '#ffd700'; spawnFloater(getFloaterPos(worldState.player.position, 3), clutchText, clutchColor); // v7.91: Use pooled position // 4. Particle burst if (particles) { const particleColor = isCritical ? 0xff0000 : 0xffd700; particles.emit(worldState.player.position, isCritical ? 60 : 40, particleColor, { spread: 6, lifetime: 1500 }); } // 5. ECHO comments on the clutch moment if (gameData.companion && gameData.companion.hp > 0) { const message = CLUTCH_ECHO_MESSAGES[Math.floor(Math.random() * CLUTCH_ECHO_MESSAGES.length)]; setTimeout(() => { addCopilotMessage(`🔥 ${message}`, 'ai'); }, 300); } // 6. Audio stinger - dramatic descending flourish playClutchStinger(isCritical); // 7. Track for statistics if (!gameData.statistics.clutchKills) gameData.statistics.clutchKills = 0; gameData.statistics.clutchKills++; // 8. Notification showNotification(isCritical ? '💀 CRITICAL CLUTCH!' : '⚡ CLUTCH KILL!', 'legendary'); } function playClutchStinger(isCritical) { if (!AudioSystem || !AudioSystem.ctx) return; try { const ctx = AudioSystem.ctx; const now = ctx.currentTime; // Dramatic chord - root + fifth + octave const frequencies = isCritical ? [220, 277.18, 329.63, 440] // A minor chord + octave (intense) : [261.63, 329.63, 392, 523.25]; // C major chord (triumphant) frequencies.forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = isCritical ? 'sawtooth' : 'triangle'; osc.frequency.value = freq; const startTime = now + (i * 0.02); gain.gain.setValueAtTime(0, startTime); gain.gain.linearRampToValueAtTime(0.15, startTime + 0.03); gain.gain.exponentialRampToValueAtTime(0.001, startTime + 0.5); osc.start(startTime); osc.stop(startTime + 0.6); }); } catch (e) { // Silently fail } } // ============================================ // v8.0: ADRENALINE SURGE - 8-Agent Consensus (Cycle 4) // Low HP grants escalating combat bonuses - risk vs. reward! // ============================================ const ADRENALINE_CONFIG = { THRESHOLD_LOW: 30, // Below 30% HP = Adrenaline active THRESHOLD_CRITICAL: 15, // Below 15% HP = Enhanced surge THRESHOLD_EXTREME: 5, // Below 5% HP = Maximum surge BONUSES: { low: { damage: 0.15, attackSpeed: 0.10, styleGain: 0.25 }, critical: { damage: 0.30, attackSpeed: 0.20, styleGain: 0.50 }, extreme: { damage: 0.50, attackSpeed: 0.35, styleGain: 1.0 } } }; function getAdrenalineSurgeLevel() { if (!gameData?.player?.hp || !gameData?.player?.maxHp) return null; const hpPercent = (gameData.player.hp / gameData.player.maxHp) * 100; if (hpPercent <= ADRENALINE_CONFIG.THRESHOLD_EXTREME) return 'extreme'; if (hpPercent <= ADRENALINE_CONFIG.THRESHOLD_CRITICAL) return 'critical'; if (hpPercent <= ADRENALINE_CONFIG.THRESHOLD_LOW) return 'low'; return null; } function getAdrenalineDamageMultiplier() { const level = getAdrenalineSurgeLevel(); if (!level) return 1.0; return 1.0 + ADRENALINE_CONFIG.BONUSES[level].damage; } function getAdrenalineStyleMultiplier() { const level = getAdrenalineSurgeLevel(); if (!level) return 1.0; return 1.0 + ADRENALINE_CONFIG.BONUSES[level].styleGain; } // Track adrenaline state changes for feedback let lastAdrenalineLevel = null; function checkAdrenalineStateChange() { const currentLevel = getAdrenalineSurgeLevel(); if (currentLevel !== lastAdrenalineLevel) { if (currentLevel && !lastAdrenalineLevel) { // Just entered adrenaline state showNotification('⚡ ADRENALINE SURGE ACTIVE!', 'buff'); if (gameData.companion?.hp > 0) { const messages = [ "Your heart rate is spiking! Channel that energy, Commander!", "Low HP but HIGH power! Don't waste this surge!", "I can feel your adrenaline from here. Fight harder!" ]; addCopilotMessage(`💉 ${messages[Math.floor(Math.random() * messages.length)]}`, 'ai'); } } else if (currentLevel && lastAdrenalineLevel && currentLevel !== lastAdrenalineLevel) { // Level changed (got lower or higher HP) if (['critical', 'extreme'].includes(currentLevel) && lastAdrenalineLevel === 'low') { showNotification(`🔥 ADRENALINE ${currentLevel.toUpperCase()}! +${Math.floor(ADRENALINE_CONFIG.BONUSES[currentLevel].damage * 100)}% DMG`, 'legendary'); } } lastAdrenalineLevel = currentLevel; } } // ============================================ // v8.0: KILLING BLOW TIME DILATION - 8-Agent Consensus (Cycle 5) // Brief slow-motion on final kills for cinematic impact! // ============================================ const TIME_DILATION_CONFIG = { enabled: true, // Time scale multipliers (lower = slower) NORMAL_KILL: 0.4, // 40% speed for regular kills ELITE_KILL: 0.3, // 30% speed for elite kills BOSS_KILL: 0.2, // 20% speed for boss kills COMBO_BONUS: 0.02, // Additional slowdown per combo hit // Duration in milliseconds (before lerp back) DURATION_NORMAL: 180, DURATION_ELITE: 280, DURATION_BOSS: 450, // Lerp back to normal time LERP_SPEED: 4.0, // How fast to return to normal // Minimum time between triggers (prevent spam) COOLDOWN: 100, // State lastTriggerTime: 0 }; let combatTimeScale = 1.0; let targetTimeScale = 1.0; let timeDilationActive = false; let timeDilationEndTime = 0; function triggerKillTimeDilation(killType = 'normal', comboCount = 0) { if (!TIME_DILATION_CONFIG.enabled) return; const now = performance.now(); if (now - TIME_DILATION_CONFIG.lastTriggerTime < TIME_DILATION_CONFIG.COOLDOWN) return; TIME_DILATION_CONFIG.lastTriggerTime = now; // Determine time scale based on kill type let baseScale, duration; switch (killType) { case 'boss': baseScale = TIME_DILATION_CONFIG.BOSS_KILL; duration = TIME_DILATION_CONFIG.DURATION_BOSS; break; case 'elite': baseScale = TIME_DILATION_CONFIG.ELITE_KILL; duration = TIME_DILATION_CONFIG.DURATION_ELITE; break; default: baseScale = TIME_DILATION_CONFIG.NORMAL_KILL; duration = TIME_DILATION_CONFIG.DURATION_NORMAL; } // Apply combo bonus (more combo = more slowdown, capped) const comboBonus = Math.min(comboCount * TIME_DILATION_CONFIG.COMBO_BONUS, 0.15); targetTimeScale = Math.max(0.15, baseScale - comboBonus); // Activate time dilation combatTimeScale = targetTimeScale; timeDilationActive = true; timeDilationEndTime = now + duration; // Visual feedback - slight vignette pulse const vignette = document.querySelector('.vignette-overlay'); if (vignette) { vignette.style.transition = 'box-shadow 0.1s ease-out'; vignette.style.boxShadow = 'inset 0 0 80px rgba(255,255,255,0.15)'; setTimeout(() => { vignette.style.boxShadow = ''; }, duration); } // Slight FOV adjustment for impact if (camera) { const originalFOV = camera.fov; camera.fov = originalFOV - 3; camera.updateProjectionMatrix(); setTimeout(() => { camera.fov = originalFOV; camera.updateProjectionMatrix(); }, duration); } } function updateTimeDilation(deltaTime) { if (!timeDilationActive) return deltaTime; const now = performance.now(); // Check if dilation period has ended if (now > timeDilationEndTime) { // Smoothly lerp back to normal time combatTimeScale += (1.0 - combatTimeScale) * TIME_DILATION_CONFIG.LERP_SPEED * (deltaTime / 1000); if (combatTimeScale > 0.98) { combatTimeScale = 1.0; timeDilationActive = false; } } // Return scaled delta time return deltaTime * combatTimeScale; } function getCombatTimeScale() { return combatTimeScale; } let victoryStreakState = { count: 0, lastVictoryTime: 0, highestStreak: 0 }; // Backwards compatibility alias let killStreakState = victoryStreakState; function updateVictoryStreak() { const now = performance.now(); const timeSinceLastVictory = now - victoryStreakState.lastVictoryTime; if (timeSinceLastVictory <= VICTORY_STREAK_CONFIG.WINDOW) { victoryStreakState.count++; } else { victoryStreakState.count = 1; } victoryStreakState.lastVictoryTime = now; // Track best streak if (victoryStreakState.count > victoryStreakState.highestStreak) { victoryStreakState.highestStreak = victoryStreakState.count; if (gameData.statistics) { gameData.statistics.highestVictoryStreak = victoryStreakState.highestStreak; } } // Check for milestone announcement const milestone = VICTORY_STREAK_CONFIG.MILESTONES[victoryStreakState.count]; if (milestone) { showNotification(milestone.name, 'buff'); if (worldState.player) { spawnFloater(worldState.player.position, milestone.name, milestone.color); } // Extra screen flash for milestones if (typeof flashVictoryCelebration === 'function') { flashVictoryCelebration(victoryStreakState.count >= 15); } // v6.80: Victory confetti for milestone streaks (8-Agent Consensus) if (victoryStreakState.count >= 10) { spawnVictoryConfetti(victoryStreakState.count >= 25 ? 150 : 80); } AudioSystem.levelUp(); // v8.0: Play musical stinger for milestone streaks (8-Agent Consensus - Audio Feedback) if (milestone.notes && typeof playStreakStinger === 'function') { playStreakStinger(milestone.notes); } } return victoryStreakState.count; } // Backwards compatibility alias function updateKillStreak() { return updateVictoryStreak(); } function getVictoryStreakXPMultiplier() { // v8.29: Add bounds checking for streak count const safeCount = Math.max(0, victoryStreakState.count || 0); const idx = Math.min(safeCount, VICTORY_STREAK_CONFIG.XP_MULTIPLIERS.length - 1); const mult = VICTORY_STREAK_CONFIG.XP_MULTIPLIERS[idx] || 1.0; // v8.29: Cap multiplier at 5x for sanity return Math.min(mult, 5); } // Backwards compatibility alias function getKillStreakXPMultiplier() { return getVictoryStreakXPMultiplier(); } function resetVictoryStreak() { if (victoryStreakState.count >= 5) { showNotification(`Streak ended at ${victoryStreakState.count} victories!`, 'info'); } victoryStreakState.count = 0; } // Backwards compatibility alias function resetKillStreak() { resetVictoryStreak(); } // v6.9: Combat Style Meter System (Agent consensus - Combat Depth & Physics) const STYLE_METER_CONFIG = { DECAY_RATE: 15, // Points lost per second MAX_POINTS: 1000, GRADES: { D: { min: 0, color: '#888888', bonus: 1.0, name: 'D' }, C: { min: 100, color: '#44ff44', bonus: 1.1, name: 'C' }, B: { min: 250, color: '#4488ff', bonus: 1.2, name: 'B' }, A: { min: 450, color: '#ffaa00', bonus: 1.35, name: 'A' }, S: { min: 650, color: '#ff44ff', bonus: 1.5, name: 'S' }, SS: { min: 800, color: '#ff0088', bonus: 1.75, name: 'SS' }, SSS: { min: 950, color: '#ffd700', bonus: 2.0, name: 'SSS' } }, ACTIONS: { hit: 15, comboHit: 25, parry: 100, dodge: 30, abilityHit: 50, defeat: 40, // v6.12: Renamed from 'kill' for family-friendly kill: 40, // Backwards compatibility finisher: 75, damageTaken: -50 } }; let styleMeterState = { points: 0, grade: 'D', lastUpdate: 0 }; // ============================================ // v8.0: STYLE VARIETY MULTIPLIER - 8-Agent Consensus (Cycle 6) // Varied combat actions escalate style gains, repeated actions decay! // ============================================ const STYLE_VARIETY_CONFIG = { HISTORY_SIZE: 6, // Track last 6 actions VARIETY_MULTIPLIERS: { 1: 1.0, // Same action = base 2: 1.5, // 2 unique = 1.5x 3: 2.0, // 3 unique = 2x 4: 2.5, // 4 unique = 2.5x 5: 3.0, // 5+ unique = 3x (cap) 6: 3.0 }, REPEAT_DECAY: 0.75, // Repeated action = 0.75x REPEAT_MINIMUM: 0.5, // Decay floor STALE_THRESHOLD: 3, // After 3 repeats, show "STALE" // Action categories (similar actions count as same) ACTION_CATEGORIES: { hit: 'attack', comboHit: 'attack', kill: 'kill', finisher: 'finisher', dodge: 'dodge', parry: 'parry', ability: 'ability' } }; let styleVarietyHistory = []; let consecutiveRepeats = 0; function getStyleVarietyMultiplier(action) { const category = STYLE_VARIETY_CONFIG.ACTION_CATEGORIES[action] || action; // Check if this is a repeat of the last action const lastCategory = styleVarietyHistory.length > 0 ? (STYLE_VARIETY_CONFIG.ACTION_CATEGORIES[styleVarietyHistory[styleVarietyHistory.length - 1]] || styleVarietyHistory[styleVarietyHistory.length - 1]) : null; if (lastCategory === category) { consecutiveRepeats++; } else { consecutiveRepeats = 0; } // Add to history styleVarietyHistory.push(action); if (styleVarietyHistory.length > STYLE_VARIETY_CONFIG.HISTORY_SIZE) { styleVarietyHistory.shift(); } // Count unique categories in history const categories = styleVarietyHistory.map(a => STYLE_VARIETY_CONFIG.ACTION_CATEGORIES[a] || a); const uniqueCategories = new Set(categories).size; // Calculate multiplier let multiplier = STYLE_VARIETY_CONFIG.VARIETY_MULTIPLIERS[uniqueCategories] || 1.0; // Apply decay for consecutive repeats if (consecutiveRepeats > 0) { const decayFactor = Math.pow(STYLE_VARIETY_CONFIG.REPEAT_DECAY, consecutiveRepeats); multiplier = Math.max(STYLE_VARIETY_CONFIG.REPEAT_MINIMUM, multiplier * decayFactor); } // Show visual feedback showStyleVarietyFeedback(uniqueCategories, consecutiveRepeats); return multiplier; } function showStyleVarietyFeedback(uniqueCount, repeats) { // Only show feedback for significant events if (repeats >= STYLE_VARIETY_CONFIG.STALE_THRESHOLD) { // Show "STALE" indicator if (!document.getElementById('style-variety-stale')) { const staleDiv = document.createElement('div'); staleDiv.id = 'style-variety-stale'; staleDiv.style.cssText = ` position: fixed; top: 190px; right: 80px; color: #aaa; font-size: 12px; font-weight: bold; letter-spacing: 2px; opacity: 0.8; animation: stalePulse 0.5s ease-out; z-index: 100; `; staleDiv.textContent = 'STALE'; document.body.appendChild(staleDiv); setTimeout(() => staleDiv.remove(), 1000); } } else if (uniqueCount >= 4) { // Show "VARIED!" indicator for high variety const variedDiv = document.createElement('div'); variedDiv.style.cssText = ` position: fixed; top: 190px; right: 80px; color: ${uniqueCount >= 5 ? '#ff00ff' : '#ffdd00'}; font-size: ${uniqueCount >= 5 ? '14px' : '12px'}; font-weight: bold; letter-spacing: 2px; text-shadow: 0 0 10px currentColor; animation: varietyPop 0.4s ease-out forwards; z-index: 100; `; variedDiv.textContent = uniqueCount >= 5 ? 'STYLISH!' : 'VARIED!'; document.body.appendChild(variedDiv); setTimeout(() => variedDiv.remove(), 600); } } // Add CSS for variety feedback animations (function addStyleVarietyCSS() { if (document.getElementById('style-variety-css')) return; const style = document.createElement('style'); style.id = 'style-variety-css'; style.textContent = ` @keyframes varietyPop { 0% { transform: scale(0.5); opacity: 0; } 50% { transform: scale(1.2); opacity: 1; } 100% { transform: scale(1); opacity: 0; } } @keyframes stalePulse { 0% { opacity: 0; } 50% { opacity: 0.8; } 100% { opacity: 0.4; } } `; document.head.appendChild(style); })(); // ============================================ // v8.0: KILL-CHAIN AUTO-TARGET LOCK - 8-Agent Consensus Cycle 6 // After scoring a kill, automatically snap to nearest enemy // Keeps combat flowing without manual re-targeting // ============================================ const AUTO_TARGET_CONFIG = { ENABLED: true, // Toggle via settings MAX_RANGE: 18, // Maximum auto-target range (units) PREFER_ELITE_RANGE: 25, // Extended range for elites/bosses MIN_DELAY: 80, // Minimum delay before targeting (ms) MAX_DELAY: 150, // Maximum delay before targeting (ms) VISUAL_DURATION: 400, // Lock-on indicator duration (ms) AUDIO_ENABLED: true, // Play lock-on sound PRIORITIZE_DAMAGED: true, // Prefer already-damaged enemies PRIORITIZE_ELITE: true // Prefer elites within range }; let autoTargetState = { lastLockTime: 0, lockIndicator: null, lockTimeout: null }; // Find the best target after a kill // v7.77: Optimized with distanceToSquared to eliminate sqrt per mob function findNextTarget(killedPosition) { if (!AUTO_TARGET_CONFIG.ENABLED) return null; if (!worldState?.mobs?.length) return null; const player = worldState.player; if (!player) return null; let bestTarget = null; let bestScore = Infinity; // v7.77: Pre-compute squared range thresholds const maxRangeSq = AUTO_TARGET_CONFIG.MAX_RANGE * AUTO_TARGET_CONFIG.MAX_RANGE; const eliteRangeSq = AUTO_TARGET_CONFIG.PREFER_ELITE_RANGE * AUTO_TARGET_CONFIG.PREFER_ELITE_RANGE; // v8.01: forEach to for loop conversion for auto-targeting hot path for (let i = 0, len = worldState.mobs.length; i < len; i++) { const mob = worldState.mobs[i]; if (!mob?.userData?.hp || mob.userData.hp <= 0) continue; const distSq = mob.position.distanceToSquared(player.position); const isElite = mob.userData.isElite || mob.userData.type === 'boss'; const maxRangeCheckSq = isElite ? eliteRangeSq : maxRangeSq; if (distSq > maxRangeCheckSq) continue; // Calculate targeting score (lower = better) - use sqrt only for scoring math const dist = Math.sqrt(distSq); let score = dist; // Prefer damaged enemies if (AUTO_TARGET_CONFIG.PRIORITIZE_DAMAGED && mob.userData.hp < mob.userData.maxHp) { score -= 5; // Extra priority for low-HP enemies (finishable) if (mob.userData.hp < mob.userData.maxHp * 0.25) { score -= 3; } } // Prefer elites if (AUTO_TARGET_CONFIG.PRIORITIZE_ELITE && isElite) { score -= 4; } // Prefer enemies in front of player (based on facing direction) // v7.95: Use GlobalVec3Pool.temp() to eliminate clone() and new Vector3() per mob const toMob = GlobalVec3Pool.tempAt(0).copy(mob.position).sub(player.position).normalize(); const playerForward = GlobalVec3Pool.tempAt(1).set(0, 0, -1).applyQuaternion(player.quaternion); const dotProduct = toMob.dot(playerForward); if (dotProduct > 0.5) score -= 3; // In front else if (dotProduct < -0.5) score += 2; // Behind if (score < bestScore) { bestScore = score; bestTarget = mob; } } return bestTarget; } // Visual lock-on indicator function showLockOnIndicator(target) { if (!target) return; // Clear existing indicator clearLockOnIndicator(); // Create lock-on ring effect const ringGeometry = new THREE.RingGeometry(0.8, 1.0, 16); const ringMaterial = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.8, side: THREE.DoubleSide }); const ring = new THREE.Mesh(ringGeometry, ringMaterial); ring.rotation.x = -Math.PI / 2; ring.position.copy(target.position); ring.position.y = 0.1; scene.add(ring); autoTargetState.lockIndicator = ring; // Animate the ring let startTime = performance.now(); const animateRing = () => { if (!autoTargetState.lockIndicator) return; const elapsed = performance.now() - startTime; const progress = elapsed / AUTO_TARGET_CONFIG.VISUAL_DURATION; if (progress >= 1) { clearLockOnIndicator(); return; } // Pulse and fade const scale = 1 + progress * 0.5; ring.scale.set(scale, scale, 1); ring.material.opacity = 0.8 * (1 - progress); ring.position.copy(target.position); ring.position.y = 0.1; requestAnimationFrame(animateRing); }; requestAnimationFrame(animateRing); } function clearLockOnIndicator() { if (autoTargetState.lockIndicator) { scene.remove(autoTargetState.lockIndicator); autoTargetState.lockIndicator.geometry?.dispose(); autoTargetState.lockIndicator.material?.dispose(); autoTargetState.lockIndicator = null; } if (autoTargetState.lockTimeout) { clearTimeout(autoTargetState.lockTimeout); autoTargetState.lockTimeout = null; } } // Lock-on audio cue function playLockOnSound(isElite = false) { if (!AUTO_TARGET_CONFIG.AUDIO_ENABLED) return; try { const ctx = AudioSystem.ctx || new (window.AudioContext || window.webkitAudioContext)(); if (ctx.state === 'suspended') ctx.resume(); const now = ctx.currentTime; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); // Quick ascending "lock-on" chirp osc.type = 'sine'; const baseFreq = isElite ? 880 : 660; osc.frequency.setValueAtTime(baseFreq * 0.8, now); osc.frequency.exponentialRampToValueAtTime(baseFreq * 1.2, now + 0.08); osc.frequency.exponentialRampToValueAtTime(baseFreq, now + 0.12); gain.gain.setValueAtTime(0.12, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15); osc.start(now); osc.stop(now + 0.15); } catch (e) { // Audio not available } } // Main auto-target function called after kills function triggerKillChainAutoTarget(killedPosition, killedType = 'normal') { if (!AUTO_TARGET_CONFIG.ENABLED) return; // Add slight delay for cinematic feel const delay = AUTO_TARGET_CONFIG.MIN_DELAY + Math.random() * (AUTO_TARGET_CONFIG.MAX_DELAY - AUTO_TARGET_CONFIG.MIN_DELAY); autoTargetState.lockTimeout = setTimeout(() => { const nextTarget = findNextTarget(killedPosition); if (nextTarget) { // Set as new target worldState.target = nextTarget; autoTargetState.lastLockTime = performance.now(); // Visual and audio feedback const isElite = nextTarget.userData?.isElite || nextTarget.userData?.type === 'boss'; showLockOnIndicator(nextTarget); playLockOnSound(isElite); // Floater feedback - v7.91: Use pooled position const targetName = nextTarget.userData?.name || 'Enemy'; spawnFloater(getFloaterPos(nextTarget.position, 2), `🎯 ${targetName}`, isElite ? '#ff44ff' : '#00ffff'); } }, delay); } // ============================================ // v8.0: RESOURCE COLLECTION MAGNET - 8-Agent Consensus Cycle 6 // Visual "magnet pull" effect when resources are collected // Drops visually fly toward the player for satisfying feedback // ============================================ const COLLECTION_MAGNET_CONFIG = { ENABLED: true, MAGNET_RANGE: 6, // Auto-collect within this range (units) PULL_SPEED: 12, // Speed items fly toward player TRAIL_LENGTH: 5, // Number of trail particles AUDIO_ENABLED: true, // Play collection sounds SPARKLE_ON_COLLECT: true, // Burst of sparkles when collected ITEM_COLORS: { 'Log': 0x8B4513, 'Ore': 0x888888, 'Slime': 0x44ff44, 'Raw Fish': 0x4488ff, 'Elite Essence': 0xaa00ff, 'Gold': 0xffd700, 'default': 0xffffff } }; let magnetState = { flyingItems: [], // Items currently flying toward player collectSoundPool: [] }; // Get color for an item type function getMagnetItemColor(itemName) { return COLLECTION_MAGNET_CONFIG.ITEM_COLORS[itemName] || COLLECTION_MAGNET_CONFIG.ITEM_COLORS['default']; } // Create a flying item visual that moves toward player function spawnMagnetItem(startPos, itemName, count = 1) { if (!COLLECTION_MAGNET_CONFIG.ENABLED) return; if (!worldState.player || !scene) return; const color = getMagnetItemColor(itemName); // Create small orb for the flying item const orbGeometry = new THREE.SphereGeometry(0.15, 8, 8); const orbMaterial = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.9 }); const orb = new THREE.Mesh(orbGeometry, orbMaterial); orb.position.copy(startPos); orb.position.y += 0.5 + Math.random() * 0.5; // Slight height variation // Add glow effect const glowGeometry = new THREE.SphereGeometry(0.25, 8, 8); const glowMaterial = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.4 }); const glow = new THREE.Mesh(glowGeometry, glowMaterial); orb.add(glow); scene.add(orb); magnetState.flyingItems.push({ mesh: orb, startPos: startPos.clone(), itemName: itemName, count: count, startTime: performance.now(), collected: false, trailParticles: [] }); } // Update flying items each frame function updateMagnetItems(deltaTime) { if (!COLLECTION_MAGNET_CONFIG.ENABLED) return; if (!worldState.player) return; const playerPos = worldState.player.position; const now = performance.now(); const pullSpeed = COLLECTION_MAGNET_CONFIG.PULL_SPEED * (deltaTime / 1000); magnetState.flyingItems = magnetState.flyingItems.filter(item => { if (item.collected) { // Clean up scene.remove(item.mesh); item.mesh.geometry?.dispose(); item.mesh.material?.dispose(); return false; } // v7.79: distanceToSquared optimization for collection magnet const distSq = item.mesh.position.distanceToSquared(playerPos); // Check if collected (0.5 * 0.5 = 0.25) if (distSq < 0.25) { item.collected = true; // Sparkle burst on collection if (COLLECTION_MAGNET_CONFIG.SPARKLE_ON_COLLECT && particles) { const color = getMagnetItemColor(item.itemName); particles.emit(item.mesh.position, 8, color, { spread: 1.5, lifetime: 400, size: 0.1 }); } // Play collection sound if (COLLECTION_MAGNET_CONFIG.AUDIO_ENABLED) { playMagnetCollectSound(item.itemName); } return false; } // Move toward player - v7.95: Use temp vector to avoid clone() per item per frame const direction = GlobalVec3Pool.temp().copy(playerPos).sub(item.mesh.position).normalize(); // v7.79: Compute dist only when needed for accel calculation const dist = Math.sqrt(distSq); const accelFactor = Math.max(0.5, 1 + (1 - dist / 10) * 2); item.mesh.position.add(direction.multiplyScalar(pullSpeed * accelFactor)); // Spin and pulse const elapsed = now - item.startTime; item.mesh.rotation.y = elapsed * 0.01; const pulse = 1 + Math.sin(elapsed * 0.015) * 0.2; item.mesh.scale.setScalar(pulse); // Spawn trail particles occasionally if (particles && Math.random() < 0.3) { const color = getMagnetItemColor(item.itemName); particles.emit(item.mesh.position, 1, color, { spread: 0.3, lifetime: 200, size: 0.08 }); } return true; }); } // Play satisfying collection sound function playMagnetCollectSound(itemName) { try { const ctx = AudioSystem.ctx || new (window.AudioContext || window.webkitAudioContext)(); if (ctx.state === 'suspended') ctx.resume(); const now = ctx.currentTime; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); // Different tones for different item types let baseFreq = 660; if (itemName === 'Elite Essence' || itemName === 'Gold') { baseFreq = 880; // Higher for rare items } else if (itemName === 'Log' || itemName === 'Ore') { baseFreq = 440; // Lower for resources } // Quick ascending "plop" sound osc.type = 'sine'; osc.frequency.setValueAtTime(baseFreq * 0.7, now); osc.frequency.exponentialRampToValueAtTime(baseFreq, now + 0.05); osc.frequency.exponentialRampToValueAtTime(baseFreq * 1.3, now + 0.1); gain.gain.setValueAtTime(0.08, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.12); osc.start(now); osc.stop(now + 0.12); } catch (e) { // Audio not available } } // Enhanced addItem that can spawn magnet visuals function addItemWithMagnet(name, sourcePosition) { if (sourcePosition && COLLECTION_MAGNET_CONFIG.ENABLED) { spawnMagnetItem(sourcePosition, name, 1); } // Note: Actual item addition still happens through original addItem } function updateStyleMeter(action, multiplier = 1) { let pointChange = (STYLE_METER_CONFIG.ACTIONS[action] || 0) * multiplier; // v8.0: Apply Style Variety Multiplier (8-Agent Consensus Cycle 6) if (typeof getStyleVarietyMultiplier === 'function' && action !== 'damageTaken') { const varietyMult = getStyleVarietyMultiplier(action); pointChange = Math.floor(pointChange * varietyMult); } // v8.0: Apply Adrenaline Surge style gain boost (8-Agent Consensus Cycle 4) if (typeof getAdrenalineStyleMultiplier === 'function') { const styleMult = getAdrenalineStyleMultiplier(); if (styleMult > 1.0) { pointChange = Math.floor(pointChange * styleMult); } } styleMeterState.points = Math.max(0, Math.min( STYLE_METER_CONFIG.MAX_POINTS, styleMeterState.points + pointChange )); updateStyleGrade(); updateStyleMeterUI(); // v8.0: Track behavioral patterns for companion commentary if (typeof trackBehaviorPattern === 'function') { if (action === 'hit' || action === 'kill' || action === 'finisher') { trackBehaviorPattern('attack'); // Check for berserker style (attacking at low HP) if (gameData?.player?.hp < 30) { trackBehaviorPattern('low_hp_attack'); } } else if (action === 'dodge' || action === 'parry') { trackBehaviorPattern('dodge'); } // Track SSS achievement if (styleMeterState.grade === 'SSS') { trackBehaviorPattern('style_sss'); } } } // ============================================ // v8.0: STYLE GRADE CELEBRATION - 8-Agent Consensus // Every grade transition should feel INCREDIBLE // ============================================ const STYLE_GRADE_EFFECTS = { 'D': { shake: 0, particles: 0, color: 0x666666 }, 'C': { shake: 0.1, particles: 8, color: 0x888888 }, 'B': { shake: 0.15, particles: 12, color: 0x44aaff }, 'A': { shake: 0.2, particles: 20, color: 0x44ff44 }, 'S': { shake: 0.3, particles: 35, color: 0xffdd00 }, 'SS': { shake: 0.4, particles: 50, color: 0xff8800 }, 'SSS': { shake: 0.5, particles: 80, color: 0xff00ff } }; function updateStyleGrade() { let newGrade = 'D'; for (const [grade, data] of Object.entries(STYLE_METER_CONFIG.GRADES)) { if (styleMeterState.points >= data.min) { newGrade = grade; } } if (newGrade !== styleMeterState.grade) { const oldGrade = styleMeterState.grade; const isUpgrade = STYLE_METER_CONFIG.GRADES[newGrade].min > STYLE_METER_CONFIG.GRADES[oldGrade].min; styleMeterState.grade = newGrade; const gradeData = STYLE_METER_CONFIG.GRADES[newGrade]; // v8.0: Celebrate EVERY grade-up with escalating feedback! if (isUpgrade) { const effects = STYLE_GRADE_EFFECTS[newGrade] || STYLE_GRADE_EFFECTS['D']; // v8.29: Add VisualFeedback for style grade upgrades if (typeof VisualFeedback !== 'undefined' && gradeData.min >= 300) { const color = '#' + effects.color.toString(16).padStart(6, '0'); VisualFeedback.successBurst(color); } // Screen shake scales with grade if (effects.shake > 0 && typeof screenShake === 'function') { screenShake(effects.shake); } // Particles burst from player if (effects.particles > 0 && particles && worldState?.player) { particles.emit(worldState.player.position, effects.particles, effects.color, { spread: 6 + (effects.particles / 10), lifetime: 1200 }); } // Audio scales with grade if (gradeData.min >= 650) { // S rank or higher showNotification(`🔥 Style Rank: ${newGrade}!`, 'legendary'); AudioSystem.levelUp(); } else if (gradeData.min >= 300) { // A/B rank showNotification(`Style Rank: ${newGrade}!`, 'buff'); if (AudioSystem.collect) AudioSystem.collect(); } else if (gradeData.min >= 100) { // C rank showNotification(`Style: ${newGrade}`, 'info'); } // Extra celebration for SSS - the ultimate achievement if (newGrade === 'SSS') { showNotification('⚡ STYLISH! ⚡', 'legendary'); setTimeout(() => { if (particles && worldState?.player) { // Second particle burst for SSS particles.emit(worldState.player.position, 100, 0xffffff, { spread: 15, lifetime: 2000 }); } }, 300); } } } } function decayStyleMeter(dt) { if (styleMeterState.points > 0) { styleMeterState.points = Math.max(0, styleMeterState.points - STYLE_METER_CONFIG.DECAY_RATE * dt); updateStyleGrade(); updateStyleMeterUI(); } } function getStyleXPMultiplier() { return STYLE_METER_CONFIG.GRADES[styleMeterState.grade]?.bonus || 1.0; } // v6.82: Cached DOM references for style meter (eliminates 3 getElementById calls per update) let _styleMeterCache = null; function getStyleMeterCache() { if (!_styleMeterCache) { _styleMeterCache = { container: document.getElementById('style-meter'), fill: document.getElementById('style-meter-fill'), grade: document.getElementById('style-meter-grade') }; } return _styleMeterCache; } function updateStyleMeterUI() { const cache = getStyleMeterCache(); if (!cache.container) return; const gradeData = STYLE_METER_CONFIG.GRADES[styleMeterState.grade]; if (cache.fill) { cache.fill.style.height = `${(styleMeterState.points / STYLE_METER_CONFIG.MAX_POINTS) * 100}%`; cache.fill.style.background = `linear-gradient(to top, ${gradeData.color}, ${gradeData.color}88)`; } if (cache.grade) { cache.grade.textContent = gradeData.name; cache.grade.style.color = gradeData.color; cache.grade.style.textShadow = `0 0 10px ${gradeData.color}`; } } // v6.9: Elemental Weakness/Resistance System (Agent consensus - Combat Depth) const ELEMENTAL_AFFINITIES = { Slime: { weak: ['fire'], resist: ['ice'], immune: [] }, Crawler: { weak: ['ice'], resist: ['void'], immune: [] }, Spitter: { weak: ['void'], resist: ['cosmic'], immune: ['fire'] }, Brute: { weak: ['cosmic'], resist: ['fire', 'ice'], immune: [] }, Warper: { weak: ['fire', 'ice'], resist: [], immune: ['void'] }, Scorpion: { weak: ['ice'], resist: ['fire'], immune: [] }, VoidSpawn: { weak: ['cosmic', 'fire'], resist: ['void'], immune: [] }, IceWisp: { weak: ['fire'], resist: [], immune: ['ice'] }, MagmaCore: { weak: ['ice'], resist: [], immune: ['fire'] }, CrystalGolem: { weak: ['void'], resist: ['ice', 'cosmic'], immune: [] }, ShadowWraith: { weak: ['cosmic'], resist: ['void'], immune: [] }, Hypnotist: { weak: ['fire'], resist: ['cosmic'], immune: [] } }; const ELEMENTAL_MULTIPLIERS = { weak: 2.0, resist: 0.5, immune: 0 }; function getElementalMultiplier(mobName, weaponElement) { if (!weaponElement || !ELEMENTAL_AFFINITIES[mobName]) { return { multiplier: 1.0, type: 'normal' }; } const affinities = ELEMENTAL_AFFINITIES[mobName]; if (affinities.immune.includes(weaponElement)) { return { multiplier: ELEMENTAL_MULTIPLIERS.immune, type: 'immune' }; } if (affinities.weak.includes(weaponElement)) { return { multiplier: ELEMENTAL_MULTIPLIERS.weak, type: 'weak' }; } if (affinities.resist.includes(weaponElement)) { return { multiplier: ELEMENTAL_MULTIPLIERS.resist, type: 'resist' }; } return { multiplier: 1.0, type: 'normal' }; } // ============================================ // v8.0: ELEMENTAL PET COMBAT - 8-Agent Consensus // Pets with biome evolution elements deal bonus damage against weak enemies! // Maps biome evolution elements to combat elemental types // ============================================ const PET_ELEMENT_TO_COMBAT = { ice: 'ice', fire: 'fire', earth: 'cosmic', // Earth pets deal cosmic damage water: 'ice', // Water = ice for weakness purposes void: 'void', light: 'cosmic', // Light = cosmic energy nature: 'fire', // Nature counters ice (forest heat) balanced: null // No elemental bonus }; function getActivePetElement() { if (!gameData.pets?.active) return null; const activePetId = gameData.pets.active; const petEvolution = gameData.petEvolution?.[activePetId]; if (!petEvolution?.element) return null; return PET_ELEMENT_TO_COMBAT[petEvolution.element] || null; } function getPetElementalBonus(mobName) { const petElement = getActivePetElement(); if (!petElement) return { multiplier: 1.0, type: 'none', element: null }; const result = getElementalMultiplier(mobName, petElement); return { multiplier: result.multiplier, type: result.type, element: petElement }; } // v6.9: Knockback with Momentum System (Agent consensus - Physics Fun) const KNOCKBACK_CONFIG = { BASE_FORCE: 3, MASS_REDUCTION: 0.5, // Elites resist knockback FRICTION: 0.92, BOUNCE_DAMPEN: 0.3, MIN_VELOCITY: 0.01 }; // v7.33: Pre-allocated temp vector for knockback (Cycle 6 Consensus - Performance) const _knockbackTemp = new THREE.Vector3(); // v7.91: Pre-allocated direction temp for applyKnockback (avoids clone per hit) const _knockbackDirTemp = new THREE.Vector3(); function applyKnockback(mob, direction, force) { // v8.25: Enhanced input validation if (!mob || !mob.userData) return; if (!direction || typeof direction.x !== 'number') return; if (typeof force !== 'number' || !isFinite(force) || force <= 0) return; const data = mob.userData; const mass = data.isElite ? 1 + KNOCKBACK_CONFIG.MASS_REDUCTION : 1; const knockbackForce = Math.min(force / mass, 100); // v8.25: Cap max knockback // v7.91: Use pre-allocated temp instead of clone() _knockbackDirTemp.copy(direction).normalize(); _knockbackDirTemp.y = 0.2; // Slight upward arc if (!data.knockbackVelocity) { data.knockbackVelocity = new THREE.Vector3(0, 0, 0); } data.knockbackVelocity.add(_knockbackDirTemp.multiplyScalar(knockbackForce)); } function updateMobKnockback(mob, dt) { // v8.25: Input validation if (!mob || !mob.userData) return; if (typeof dt !== 'number' || !isFinite(dt) || dt <= 0) return; const data = mob.userData; if (!data.knockbackVelocity) return; if (data.knockbackVelocity.lengthSq() > KNOCKBACK_CONFIG.MIN_VELOCITY) { // v7.33: Use pre-allocated vector to avoid GC (Cycle 6 Consensus) _knockbackTemp.copy(data.knockbackVelocity).multiplyScalar(dt); mob.position.add(_knockbackTemp); data.knockbackVelocity.multiplyScalar(KNOCKBACK_CONFIG.FRICTION); // Ground collision if (mob.position.y < 0.8) { mob.position.y = 0.8; data.knockbackVelocity.y *= -KNOCKBACK_CONFIG.BOUNCE_DAMPEN; } } } // v6.9: Lore Fragment Collection System (Agent consensus - Secrets & Meta) const LORE_FRAGMENTS = { origin_exodus: { title: 'The First Exodus', text: 'Long ago, humanity fled a dying Earth aboard the Leviathan ships...', icon: '📜', rarity: 0.05, trigger: 'explore' }, origin_gate: { title: 'The Omniverse Gate', text: 'Scientists discovered a rift that led to infinite parallel dimensions...', icon: '📜', rarity: 0.03, trigger: 'boss_defeat' }, void_warning: { title: 'Whispers of the Void', text: 'The void creatures are not invaders. They are refugees, fleeing something worse...', icon: '📜', rarity: 0.04, trigger: 'explore' }, ancient_tech: { title: 'Forgotten Technology', text: 'The ancients built machines that could reshape reality itself...', icon: '📜', rarity: 0.03, trigger: 'craft' }, cosmic_truth: { title: 'The Cosmic Truth', text: 'Every universe in the omniverse is connected by threads of pure energy...', icon: '📜', rarity: 0.02, trigger: 'explore' }, leviathan_secret: { title: 'The Leviathan Protocol', text: 'The ships were never meant for colonization. They were weapons...', icon: '🔮', rarity: 0.01, trigger: 'boss_defeat' } }; function tryDiscoverLore(triggerType) { if (!gameData.loreFragments) gameData.loreFragments = {}; const eligible = Object.entries(LORE_FRAGMENTS).filter(([id, lore]) => { if (gameData.loreFragments[id]) return false; if (lore.trigger !== triggerType) return false; return Math.random() < lore.rarity; }); if (eligible.length > 0) { const [id, lore] = eligible[Math.floor(Math.random() * eligible.length)]; discoverLoreFragment(id); } } function discoverLoreFragment(id) { const lore = LORE_FRAGMENTS[id]; if (!lore || gameData.loreFragments[id]) return; gameData.loreFragments[id] = { discoveredAt: Date.now() }; // v6.35: Chronicle Engine - capture lore discovery if (typeof captureChronicleEvent === 'function') { captureChronicleEvent('lore_found', { loreTitle: lore.title, loreIcon: lore.icon }); } showNotification(`${lore.icon} LORE DISCOVERED: ${lore.title}`, 'legendary'); AudioSystem.levelUp(); // Show lore popup const popup = document.createElement('div'); popup.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: linear-gradient(135deg, rgba(40, 20, 60, 0.95), rgba(20, 10, 40, 0.95)); border: 2px solid #8844ff; border-radius: 15px; padding: 30px; max-width: 400px; text-align: center; z-index: 2000; box-shadow: 0 0 30px rgba(136, 68, 255, 0.5); `; popup.innerHTML = `
${lore.icon}
${lore.title}
${lore.text}
`; document.body.appendChild(popup); saveGameData(); } function getLoreCollectionProgress() { const total = Object.keys(LORE_FRAGMENTS).length; const found = Object.keys(gameData.loreFragments || {}).length; return { found, total, percent: Math.round((found / total) * 100) }; } // v6.12: Bestiary Victory Milestones (renamed from Kill for family-friendly) const BESTIARY_MILESTONES = { Slime: { kills: [10, 50, 100], rewards: ['title:Slime Champion', 'bonus:slimeDamage:0.1', 'item:Slime Crown'] }, Crawler: { kills: [10, 50, 100], rewards: ['title:Bug Expert', 'bonus:crawlerDamage:0.1', 'item:Crawler Fang'] }, Scorpion: { kills: [10, 50, 100], rewards: ['title:Desert Champion', 'bonus:scorpionDamage:0.1', 'item:Scorpion Tail'] }, VoidSpawn: { kills: [10, 50, 100], rewards: ['title:Void Walker', 'bonus:voidDamage:0.15', 'item:Void Heart'] }, IceWisp: { kills: [10, 50, 100], rewards: ['title:Frost Champion', 'bonus:iceDamage:0.1', 'item:Frozen Soul'] }, MagmaCore: { kills: [10, 50, 100], rewards: ['title:Flame Master', 'bonus:fireDamage:0.1', 'item:Magma Core'] }, CrystalGolem: { kills: [10, 50, 100], rewards: ['title:Golem Champion', 'bonus:golemDamage:0.15', 'item:Crystal Heart'] }, ShadowWraith: { kills: [10, 50, 100], rewards: ['title:Shadow Expert', 'bonus:shadowDamage:0.15', 'item:Wraith Essence'] } }; function checkBestiaryMilestone(mobName) { if (!BESTIARY_MILESTONES[mobName]) return; if (!gameData.bestiaryProgress) gameData.bestiaryProgress = {}; if (!gameData.bestiaryProgress[mobName]) { gameData.bestiaryProgress[mobName] = { kills: 0, milestonesReached: [] }; } const progress = gameData.bestiaryProgress[mobName]; progress.kills++; const milestones = BESTIARY_MILESTONES[mobName]; milestones.kills.forEach((threshold, idx) => { if (progress.kills >= threshold && !progress.milestonesReached.includes(idx)) { progress.milestonesReached.push(idx); grantBestiaryReward(mobName, milestones.rewards[idx], threshold); } }); } function grantBestiaryReward(mobName, reward, killCount) { const [type, ...args] = reward.split(':'); switch (type) { case 'title': showNotification(`🏆 Title Earned: ${args[0]}!`, 'legendary'); if (!gameData.titles) gameData.titles = []; gameData.titles.push(args[0]); break; case 'bonus': if (!gameData.bestiaryBonuses) gameData.bestiaryBonuses = {}; gameData.bestiaryBonuses[args[0]] = parseFloat(args[1]); showNotification(`💪 +${parseFloat(args[1]) * 100}% damage vs ${mobName}!`, 'buff'); break; case 'item': addItem(args[0]); showNotification(`🎁 Bestiary Reward: ${args[0]}!`, 'success'); break; } AudioSystem.levelUp(); saveGameData(); } function getBestiaryDamageBonus(mobName) { if (!gameData.bestiaryBonuses || !mobName) return 0; const bonusKey = `${mobName.toLowerCase()}Damage`; return gameData.bestiaryBonuses[bonusKey] || 0; } // ============================================ // v6.13: WAVE MOMENTUM SYSTEM (DOTA-style creep pushing) // ============================================ // Players influence territorial control by defeating enemies and helping allies // Momentum shifts determine world bonuses and spawn rates const WAVE_CONFIG = { WAVE_INTERVAL: 30000, // New wave every 30 seconds CREEPS_PER_WAVE: 3, // Creeps spawned per side per wave MOMENTUM_DECAY: 0.5, // Momentum decays toward 50 per second PLAYER_INFLUENCE: 5, // Momentum gained per enemy defeated near front CLASH_DAMAGE: 5, // Damage creeps deal to each other per tick CLASH_INTERVAL: 1500, // Creeps attack every 1.5 seconds FRONT_LINE_SPEED: 0.3, // How fast front line moves based on momentum MOMENTUM_XP_BONUS: 0.5, // +50% XP when momentum > 70 MOMENTUM_RESOURCE_BONUS: 0.3, // +30% resources when momentum > 60 // Faction definitions FACTIONS: { explorer: { name: 'Explorer Drones', color: 0x00ff88, icon: '🤖', baseHp: 30, damage: 8 }, horde: { name: 'Void Horde', color: 0xff4488, icon: '👾', baseHp: 35, damage: 10 } } }; // Wave system state let waveState = { enabled: false, momentum: 50, // 0-100: 0=horde winning, 100=explorers winning frontLineZ: 0, // Z position of the "front line" explorerCreeps: [], // Active friendly creeps hordeCreeps: [], // Active enemy creeps lastWaveTime: 0, waveNumber: 0, totalExplorerDefeats: 0, totalHordeDefeats: 0, playerContribution: 0, // Track player's influence lastClashTime: 0 }; // v7.98: Spatial grid for wave creeps - O(n) neighbor lookup instead of O(n^2) const WaveCreepSpatialGrid = { cellSize: 10, // Slightly larger than clash range (8) for efficient neighbor lookup explorerGrid: new Map(), hordeGrid: new Map(), _intKey(cellX, cellZ) { return (cellX + 10000) * 100000 + (cellZ + 10000); }, rebuild() { this.explorerGrid.clear(); this.hordeGrid.clear(); // Index explorers for (let i = 0; i < waveState.explorerCreeps.length; i++) { const creep = waveState.explorerCreeps[i]; if (!creep.isAlive || !creep.mesh) continue; const cellX = Math.floor(creep.mesh.position.x / this.cellSize); const cellZ = Math.floor(creep.mesh.position.z / this.cellSize); const key = this._intKey(cellX, cellZ); if (!this.explorerGrid.has(key)) this.explorerGrid.set(key, []); this.explorerGrid.get(key).push(creep); } // Index horde for (let i = 0; i < waveState.hordeCreeps.length; i++) { const creep = waveState.hordeCreeps[i]; if (!creep.isAlive || !creep.mesh) continue; const cellX = Math.floor(creep.mesh.position.x / this.cellSize); const cellZ = Math.floor(creep.mesh.position.z / this.cellSize); const key = this._intKey(cellX, cellZ); if (!this.hordeGrid.has(key)) this.hordeGrid.set(key, []); this.hordeGrid.get(key).push(creep); } }, getNearbyHorde(x, z) { const cellX = Math.floor(x / this.cellSize); const cellZ = Math.floor(z / this.cellSize); const nearby = []; for (let dx = -1; dx <= 1; dx++) { for (let dz = -1; dz <= 1; dz++) { const cell = this.hordeGrid.get(this._intKey(cellX + dx, cellZ + dz)); if (cell) { for (let i = 0; i < cell.length; i++) { nearby.push(cell[i]); } } } } return nearby; }, getNearbyExplorers(x, z) { const cellX = Math.floor(x / this.cellSize); const cellZ = Math.floor(z / this.cellSize); const nearby = []; for (let dx = -1; dx <= 1; dx++) { for (let dz = -1; dz <= 1; dz++) { const cell = this.explorerGrid.get(this._intKey(cellX + dx, cellZ + dz)); if (cell) { for (let i = 0; i < cell.length; i++) { nearby.push(cell[i]); } } } } return nearby; } }; // Initialize wave system when entering world function initWaveSystem() { // v9.10: Skip for custom worlds - prevents unwanted wave creep spawning if (window.WORLD_SYSTEMS?.customOnly === true) { console.log('[WORLD] Skipping wave system for customOnly world'); if (typeof waveState !== 'undefined') waveState.enabled = false; return; } if (window.WORLD_SYSTEMS?.creepWaves === false) { console.log('[WORLD] Skipping wave system - creepWaves disabled'); return; } waveState = { enabled: true, momentum: 50, frontLineZ: 0, explorerCreeps: [], hordeCreeps: [], lastWaveTime: performance.now(), waveNumber: 0, totalExplorerDefeats: 0, totalHordeDefeats: 0, playerContribution: 0, lastClashTime: 0 }; updateWaveMomentumUI(); } // Create a creep for a faction function createWaveCreep(faction, waveNum) { const factionData = WAVE_CONFIG.FACTIONS[faction]; const isExplorer = faction === 'explorer'; // Scale stats with wave number const scaleFactor = 1 + (waveNum * 0.1); const hp = Math.floor(factionData.baseHp * scaleFactor); const damage = Math.floor(factionData.damage * scaleFactor); // Create mesh const geo = new THREE.SphereGeometry(0.5, 12, 12); const mat = new THREE.MeshStandardMaterial({ color: factionData.color, emissive: factionData.color, emissiveIntensity: 0.3, roughness: 0.4 }); const mesh = new THREE.Mesh(geo, mat); // Position: explorers spawn from player side, horde from opposite const spawnZ = isExplorer ? -30 : 30; const spawnX = (Math.random() - 0.5) * 20; mesh.position.set(spawnX, 1.2, spawnZ); mesh.castShadow = true; // Add faction indicator ring const ringGeo = new THREE.RingGeometry(0.6, 0.8, 16); const ringMat = new THREE.MeshBasicMaterial({ color: factionData.color, side: THREE.DoubleSide, transparent: true, opacity: 0.5 }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.rotation.x = -Math.PI / 2; ring.position.y = -0.3; mesh.add(ring); const creep = { mesh: mesh, faction: faction, hp: hp, maxHp: hp, damage: damage, isAlive: true, target: null, waveNum: waveNum }; if (scene) scene.add(mesh); return creep; } // Spawn a wave of creeps for both factions function spawnWave() { if (!waveState.enabled || mode !== 'world') return; // v9.10: Extra guard for customOnly worlds if (window.WORLD_SYSTEMS?.customOnly === true) return; waveState.waveNumber++; const waveNum = waveState.waveNumber; // Spawn explorer drones (friendly) for (let i = 0; i < WAVE_CONFIG.CREEPS_PER_WAVE; i++) { const creep = createWaveCreep('explorer', waveNum); waveState.explorerCreeps.push(creep); } // Spawn horde creeps (enemy) for (let i = 0; i < WAVE_CONFIG.CREEPS_PER_WAVE; i++) { const creep = createWaveCreep('horde', waveNum); waveState.hordeCreeps.push(creep); } // Announce wave if (waveNum % 5 === 0) { showNotification(`⚔️ Wave ${waveNum} incoming!`, 'info'); } waveState.lastWaveTime = performance.now(); } // Update wave system each frame function updateWaveSystem(dt) { if (!waveState.enabled || mode !== 'world') return; const now = performance.now(); // Spawn new waves periodically if (now - waveState.lastWaveTime > WAVE_CONFIG.WAVE_INTERVAL) { spawnWave(); } // Move creeps toward front line const frontZ = waveState.frontLineZ; // Explorer creeps move toward positive Z (toward horde) for (const creep of waveState.explorerCreeps) { if (!creep.isAlive) continue; if (creep.mesh.position.z < frontZ + 5) { creep.mesh.position.z += dt * 2; } // Rotate ring for visual effect if (creep.mesh.children[0]) { creep.mesh.children[0].rotation.z += dt * 2; } } // Horde creeps move toward negative Z (toward explorers) for (const creep of waveState.hordeCreeps) { if (!creep.isAlive) continue; if (creep.mesh.position.z > frontZ - 5) { creep.mesh.position.z -= dt * 2; } if (creep.mesh.children[0]) { creep.mesh.children[0].rotation.z -= dt * 2; } } // Creep combat (clash at front line) if (now - waveState.lastClashTime > WAVE_CONFIG.CLASH_INTERVAL) { processCreepClash(); waveState.lastClashTime = now; } // Update front line based on momentum const momentumForce = (waveState.momentum - 50) / 50; // -1 to 1 waveState.frontLineZ += momentumForce * WAVE_CONFIG.FRONT_LINE_SPEED * dt; waveState.frontLineZ = Math.max(-25, Math.min(25, waveState.frontLineZ)); // Decay momentum toward 50 (balance) if (waveState.momentum > 50) { waveState.momentum = Math.max(50, waveState.momentum - WAVE_CONFIG.MOMENTUM_DECAY * dt); } else if (waveState.momentum < 50) { waveState.momentum = Math.min(50, waveState.momentum + WAVE_CONFIG.MOMENTUM_DECAY * dt); } // Clean up defeated creeps cleanupDefeatedCreeps(); // Update UI updateWaveMomentumUI(); } // Process combat between creeps at front line // v6.82: Optimized creep clash with pre-filtered arrays and squared distance // v7.98: Use WaveCreepSpatialGrid for O(n) lookup instead of O(n^2) function processCreepClash() { const clashRange = 8; const clashRangeSq = clashRange * clashRange; // Avoid sqrt in distance checks // Early exit if no combat possible const explorerCount = waveState.explorerCreeps.length; const hordeCount = waveState.hordeCreeps.length; if (explorerCount === 0 || hordeCount === 0) return; // v7.98: Rebuild spatial grid for this frame's positions WaveCreepSpatialGrid.rebuild(); // Each explorer attacks nearest horde creep in range for (let i = 0; i < explorerCount; i++) { const explorer = waveState.explorerCreeps[i]; if (!explorer.isAlive || !explorer.mesh) continue; let nearestHorde = null; let nearestDistSq = Infinity; const ePos = explorer.mesh.position; // v7.98: Use spatial grid for O(1) neighbor lookup const nearbyHorde = WaveCreepSpatialGrid.getNearbyHorde(ePos.x, ePos.z); for (let j = 0; j < nearbyHorde.length; j++) { const horde = nearbyHorde[j]; if (!horde.isAlive) continue; // May have died this frame const hPos = horde.mesh.position; // Use squared distance to avoid expensive sqrt const dx = ePos.x - hPos.x; const dz = ePos.z - hPos.z; const distSq = dx * dx + dz * dz; if (distSq < clashRangeSq && distSq < nearestDistSq) { nearestDistSq = distSq; nearestHorde = horde; } } if (nearestHorde) { nearestHorde.hp -= explorer.damage; // Visual feedback if (particles) { particles.emit(nearestHorde.mesh.position, 2, 0x00ff88, { spread: 0.5, lifetime: 300 }); } if (nearestHorde.hp <= 0) { nearestHorde.isAlive = false; waveState.totalHordeDefeats++; waveState.momentum = Math.min(100, waveState.momentum + 2); } } } // Each horde attacks nearest explorer in range for (let i = 0; i < hordeCount; i++) { const horde = waveState.hordeCreeps[i]; if (!horde.isAlive || !horde.mesh) continue; let nearestExplorer = null; let nearestDistSq = Infinity; const hPos = horde.mesh.position; // v7.98: Use spatial grid for O(1) neighbor lookup const nearbyExplorers = WaveCreepSpatialGrid.getNearbyExplorers(hPos.x, hPos.z); for (let j = 0; j < nearbyExplorers.length; j++) { const explorer = nearbyExplorers[j]; if (!explorer.isAlive) continue; // May have died this frame const ePos = explorer.mesh.position; // Use squared distance to avoid expensive sqrt const dx = hPos.x - ePos.x; const dz = hPos.z - ePos.z; const distSq = dx * dx + dz * dz; if (distSq < clashRangeSq && distSq < nearestDistSq) { nearestDistSq = distSq; nearestExplorer = explorer; } } if (nearestExplorer) { nearestExplorer.hp -= horde.damage; // Visual feedback if (particles) { particles.emit(nearestExplorer.mesh.position, 2, 0xff4488, { spread: 0.5, lifetime: 300 }); } if (nearestExplorer.hp <= 0) { nearestExplorer.isAlive = false; waveState.totalExplorerDefeats++; waveState.momentum = Math.max(0, waveState.momentum - 2); } } } } // Clean up defeated creeps function cleanupDefeatedCreeps() { // v6.32: Helper to properly dispose mesh and its children function disposeMesh(mesh) { if (!mesh) return; scene.remove(mesh); mesh.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(m => m.dispose()); } else { child.material.dispose(); } } }); } // Remove defeated explorer creeps waveState.explorerCreeps = waveState.explorerCreeps.filter(creep => { if (!creep.isAlive && creep.mesh) { disposeMesh(creep.mesh); return false; } return true; }); // Remove defeated horde creeps waveState.hordeCreeps = waveState.hordeCreeps.filter(creep => { if (!creep.isAlive && creep.mesh) { disposeMesh(creep.mesh); return false; } return true; }); } // Player defeats a horde creep - gains momentum function onPlayerDefeatHordeCreep(creep) { if (!waveState.enabled) return; waveState.momentum = Math.min(100, waveState.momentum + WAVE_CONFIG.PLAYER_INFLUENCE); waveState.playerContribution++; waveState.totalHordeDefeats++; // Bonus XP for wave contribution const bonusXP = 15 + (waveState.waveNumber * 2); if (typeof addXp === 'function') { addXp('combat', bonusXP); } spawnFloater(creep.mesh.position, `⚔️ +${bonusXP} WAVE XP`, '#00ff88'); // Milestone rewards if (waveState.playerContribution % 10 === 0) { showNotification(`🎖️ Wave Hero! ${waveState.playerContribution} enemies pushed back!`, 'success'); } } // Get momentum bonuses for player function getWaveMomentumBonuses() { if (!waveState.enabled) return { xp: 1, resources: 1 }; const momentum = waveState.momentum; let xpMult = 1; let resourceMult = 1; if (momentum >= 70) { xpMult = 1 + WAVE_CONFIG.MOMENTUM_XP_BONUS; } if (momentum >= 60) { resourceMult = 1 + WAVE_CONFIG.MOMENTUM_RESOURCE_BONUS; } return { xp: xpMult, resources: resourceMult }; } // v6.13: Wave momentum UI - HIDDEN for implicit discovery // Players discover the wave/momentum system through exploration // The creeps fighting in the world ARE the visual feedback function updateWaveMomentumUI() { // UI intentionally hidden - system is discoverable through gameplay // The creeps fighting, visual effects, and momentum bonuses are the feedback // Players who observe and engage learn the system organically } // Check if a mob is a horde creep (for player combat integration) function isHordeCreep(target) { if (!waveState.enabled) return false; for (const creep of waveState.hordeCreeps) { if (creep.mesh === target && creep.isAlive) { return creep; } } return null; } // Cleanup wave system when leaving world function cleanupWaveSystem() { // Remove all creep meshes for (const creep of waveState.explorerCreeps) { if (creep.mesh && scene) scene.remove(creep.mesh); } for (const creep of waveState.hordeCreeps) { if (creep.mesh && scene) scene.remove(creep.mesh); } waveState.enabled = false; waveState.explorerCreeps = []; waveState.hordeCreeps = []; } // ============================================ // END WAVE MOMENTUM SYSTEM // ============================================ // v4.8: Combat Abilities System // v12.17: Added powerCost for unified battery system const COMBAT_ABILITIES = { powerStrike: { name: 'Power Strike', key: 'Q', icon: '⚔️', cooldown: 8000, // 8 seconds unlockLevel: 3, // Combat level 3 damageMultiplier: 3, powerCost: 5, // v12.17: Power drain description: '3x damage attack' }, whirlwind: { name: 'Whirlwind', key: 'E', icon: '🌀', cooldown: 12000, // 12 seconds unlockLevel: 5, // Combat level 5 radius: 8, damageMultiplier: 1.5, powerCost: 8, // v12.17: Power drain description: 'AoE damage to all nearby enemies' }, warcry: { name: 'War Cry', key: 'R', icon: '📢', cooldown: 20000, // 20 seconds unlockLevel: 7, // Combat level 7 duration: 5000, // 5 second buff damageBoost: 1.5, powerCost: 6, // v12.17: Power drain description: '+50% damage for 5 seconds' }, // v4.9: Tier 2 Abilities heal: { name: 'Battle Heal', key: 'T', icon: '💚', cooldown: 15000, // 15 seconds unlockLevel: 9, // Combat level 9 healAmount: 0.3, // 30% of max HP powerCost: 12, // v12.17: Power drain (high cost - converts power to HP) description: 'Restore 30% of max HP' }, dash: { name: 'Combat Dash', key: 'F', icon: '💨', cooldown: 6000, // 6 seconds unlockLevel: 10, // Combat level 10 distance: 8, damageMultiplier: 1.2, powerCost: 4, // v12.17: Power drain (low - mobility skill) description: 'Dash forward, damaging enemies in path' }, shieldWall: { name: 'Shield Wall', key: 'Z', icon: '🛡️', cooldown: 25000, // 25 seconds unlockLevel: 12, // Combat level 12 duration: 4000, // 4 seconds damageReduction: 0.7, // 70% damage reduction powerCost: 10, // v12.17: Power drain description: '70% damage reduction for 4 seconds' }, execute: { name: 'Execute', key: 'X', icon: '💀', cooldown: 10000, // 10 seconds unlockLevel: 15, // Combat level 15 threshold: 0.5, // v7.34: Raised from 0.3 to 0.5 (50% HP) - Cycle 13 Game Balance damageMultiplier: 3, // v7.34: Reduced from 5x to 3x (balanced for higher threshold) - Cycle 13 cooldownResetOnKill: true, // v7.34: Cooldown resets on kill - creates risk/reward loop powerCost: 7, // v12.17: Power drain description: '3x damage to enemies below 50% HP (resets cooldown on kill)' }, berserk: { name: 'Berserker Rage', key: 'C', icon: '🔥', cooldown: 45000, // 45 seconds (ultimate) unlockLevel: 20, // Combat level 20 duration: 8000, // 8 seconds damageBoost: 2.0, // 100% more damage attackSpeedBoost: 1.5,// 50% faster attacks powerCost: 15, // v12.17: Power drain (ultimate - high cost) description: 'ULTIMATE: +100% damage, +50% attack speed for 8s' }, // v6.42: CHRONO-ECHO COMBAT (Time + Sensory + Combat) // Past actions replay as ghost clones that repeat your attacks chronoEcho: { name: 'Chrono-Echo', key: 'B', icon: '👻', cooldown: 30000, // 30 seconds unlockLevel: 18, // Combat level 18 duration: 6000, // 6 seconds of ghost echoes echoCount: 5, // Number of ghost clones damageMultiplier: 0.6,// Each ghost deals 60% damage powerCost: 12, // v12.17: Power drain description: 'Summon time-echoes that replay your past attacks' } }; let abilityState = { powerStrike: { lastUsed: 0 }, whirlwind: { lastUsed: 0 }, warcry: { lastUsed: 0, activeUntil: 0 }, // v4.9: Tier 2 ability states heal: { lastUsed: 0 }, dash: { lastUsed: 0 }, shieldWall: { lastUsed: 0, activeUntil: 0 }, execute: { lastUsed: 0 }, berserk: { lastUsed: 0, activeUntil: 0 }, // v6.42: Chrono-Echo state chronoEcho: { lastUsed: 0, activeUntil: 0 } }; // ============================================ // v7.69: ABILITY SYNERGY SUGGESTION SYSTEM (8-Agent Consensus Cycle 44) // Teaches players powerful ability combos through UI hints // ============================================ const AbilitySynergySystem = { lastAbilityUsed: null, suggestionTimeout: null, synergyWindow: 4000, // 4s window to see synergy hints // Synergy definitions: ability -> recommended follow-ups synergies: { warcry: [ { ability: 'powerStrike', reason: 'WAR CRY + POWER STRIKE = Devastating damage!', icon: '💥' }, { ability: 'whirlwind', reason: 'WAR CRY + WHIRLWIND = AoE destruction!', icon: '🌪️' } ], dash: [ { ability: 'whirlwind', reason: 'DASH + WHIRLWIND = Spinning impact!', icon: '🌀' }, { ability: 'powerStrike', reason: 'DASH + POWER STRIKE = Momentum strike!', icon: '⚔️' } ], berserk: [ { ability: 'execute', reason: 'BERSERK + EXECUTE = Unstoppable finisher!', icon: '☠️' }, { ability: 'whirlwind', reason: 'BERSERK + WHIRLWIND = Spinning fury!', icon: '🔥' } ], shieldWall: [ { ability: 'heal', reason: 'SHIELD + HEAL = Full recovery combo!', icon: '🛡️' } ], whirlwind: [ { ability: 'execute', reason: 'WHIRLWIND + EXECUTE = Clean up weakened foes!', icon: '⚡' } ], chronoEcho: [ { ability: 'berserk', reason: 'CHRONO-ECHO + BERSERK = Double the chaos!', icon: '⏱️' } ] }, // Show synergy suggestion after ability use suggestCombo(usedAbility) { this.lastAbilityUsed = usedAbility; clearTimeout(this.suggestionTimeout); const synergies = this.synergies[usedAbility]; if (!synergies || synergies.length === 0) return; // Filter to only ready abilities const readysynergies = synergies.filter(s => isAbilityReady(s.ability) && isAbilityUnlocked(s.ability)); if (readysynergies.length === 0) return; // Pick random suggestion from ready abilities const suggestion = readysynergies[Math.floor(Math.random() * readysynergies.length)]; // Show floating hint near ability bar this.showSuggestionHint(suggestion); // Clear suggestion after window expires this.suggestionTimeout = setTimeout(() => { this.lastAbilityUsed = null; this.hideSuggestionHint(); }, this.synergyWindow); }, showSuggestionHint(suggestion) { let hintEl = document.getElementById('synergy-hint'); if (!hintEl) { hintEl = document.createElement('div'); hintEl.id = 'synergy-hint'; hintEl.style.cssText = ` position: fixed; bottom: 100px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, rgba(255,100,0,0.95), rgba(255,180,0,0.95)); color: #000; padding: 12px 20px; border-radius: 8px; font-size: 14px; font-weight: bold; z-index: 1000; pointer-events: none; text-align: center; box-shadow: 0 4px 20px rgba(255,150,0,0.6), 0 0 40px rgba(255,100,0,0.3); border: 2px solid rgba(255,200,0,0.8); animation: synergySuggestionPulse 0.5s ease-out; `; document.body.appendChild(hintEl); // Add animation if (!document.getElementById('synergy-animation-style')) { const style = document.createElement('style'); style.id = 'synergy-animation-style'; style.textContent = ` @keyframes synergySuggestionPulse { 0% { opacity: 0; transform: translateX(-50%) scale(0.8) translateY(10px); } 50% { transform: translateX(-50%) scale(1.05) translateY(0); } 100% { opacity: 1; transform: translateX(-50%) scale(1) translateY(0); } } `; document.head.appendChild(style); } } hintEl.innerHTML = `${suggestion.icon} TRY: ${suggestion.reason}`; hintEl.style.display = 'block'; }, hideSuggestionHint() { const hintEl = document.getElementById('synergy-hint'); if (hintEl) { hintEl.style.animation = 'fadeOut 0.3s ease-out'; setTimeout(() => { hintEl.style.display = 'none'; }, 300); } } }; // ============================================ // v8.0: COMBO COOLDOWN ACCELERATION - 8-Agent Consensus (Cycle 8) // Perfect combo chains reduce ability cooldowns // ============================================ const COMBO_CDR_CONFIG = { ENABLED: true, BASE_REDUCTION: 50, // ms per combo hit PERFECT_MULTIPLIER: 2.0, // 2x reduction for perfect timing MILESTONE_BONUSES: { // Extra reduction at milestones 3: 300, 5: 500, 10: 1000 }, FINISHER_BONUS: 1500, // Big reduction on finisher MAX_REDUCTION_PERCENT: 0.5, // Cap at 50% CDR per combo SHOW_FLOATERS: true // Show CDR feedback }; let comboCDRState = { totalReductionThisCombo: 0, lastMilestoneReached: 0 }; function applyComboCoolddownReduction(comboCount, isPerfect, isFinisher) { if (!COMBO_CDR_CONFIG.ENABLED) return; const now = performance.now(); let reduction = COMBO_CDR_CONFIG.BASE_REDUCTION; // Perfect timing doubles reduction if (isPerfect) { reduction *= COMBO_CDR_CONFIG.PERFECT_MULTIPLIER; } // Apply milestone bonus for (const [milestone, bonus] of Object.entries(COMBO_CDR_CONFIG.MILESTONE_BONUSES)) { if (comboCount === parseInt(milestone) && comboCDRState.lastMilestoneReached < parseInt(milestone)) { reduction += bonus; comboCDRState.lastMilestoneReached = parseInt(milestone); // Special feedback for milestone - v7.91: Use pooled position if (worldState.player && COMBO_CDR_CONFIG.SHOW_FLOATERS) { spawnFloater(getFloaterPos(worldState.player.position, 1.5), `⚡ COMBO ${milestone}x CDR!`, '#00ffff'); } } } // Finisher bonus if (isFinisher) { reduction += COMBO_CDR_CONFIG.FINISHER_BONUS; } // Apply reduction to all abilities - v8.08: forEach to for loop let totalApplied = 0; const abilityKeys = Object.keys(abilityState); for (let i = 0; i < abilityKeys.length; i++) { const abilityKey = abilityKeys[i]; const ability = COMBAT_ABILITIES[abilityKey]; if (!ability) continue; const elapsed = now - abilityState[abilityKey].lastUsed; const remaining = Math.max(0, ability.cooldown - elapsed); if (remaining > 0) { // Calculate max reduction for this ability const maxReduction = ability.cooldown * COMBO_CDR_CONFIG.MAX_REDUCTION_PERCENT; const currentTotalReduction = comboCDRState.totalReductionThisCombo; // Cap based on what we've already reduced this combo const allowedReduction = Math.min(reduction, maxReduction - currentTotalReduction); if (allowedReduction > 0) { // Move lastUsed back in time (effectively reducing remaining cooldown) abilityState[abilityKey].lastUsed -= allowedReduction; totalApplied += allowedReduction; } } } comboCDRState.totalReductionThisCombo += totalApplied; // Visual feedback if (totalApplied > 0 && COMBO_CDR_CONFIG.SHOW_FLOATERS) { // Glow pulse on ability bar const abilityBar = document.getElementById('ability-bar'); if (abilityBar) { abilityBar.style.boxShadow = '0 0 15px #00ffff'; setTimeout(() => { if (abilityBar) abilityBar.style.boxShadow = ''; }, 150); } // Small audio tick try { const ctx = AudioSystem.ctx || new (window.AudioContext || window.webkitAudioContext)(); if (ctx.state === 'suspended') ctx.resume(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(1200 + comboCount * 50, ctx.currentTime); gain.gain.setValueAtTime(0.05, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.08); osc.start(); osc.stop(ctx.currentTime + 0.08); } catch (e) {} } } // Reset CDR state when combo ends function resetComboCDRState() { comboCDRState.totalReductionThisCombo = 0; comboCDRState.lastMilestoneReached = 0; } // ============================================ // v8.0: ABILITY READY AUDIO CUE - 8-Agent Consensus (Cycle 4) // Satisfying audio feedback when abilities come off cooldown // ============================================ const ABILITY_READY_AUDIO = { // Track which abilities were on cooldown last frame previousCooldownState: {}, // Unique audio signatures per ability tier AUDIO_PROFILES: { // Tier 1 abilities - simple chime tier1: { baseFreq: 880, type: 'sine', duration: 0.15, decay: 0.8 }, // Tier 2 abilities - richer tone tier2: { baseFreq: 660, type: 'triangle', duration: 0.2, decay: 0.7 }, // Ultimate abilities - epic fanfare ultimate: { baseFreq: 440, type: 'square', duration: 0.3, decay: 0.6 } }, // Map abilities to their audio tier ABILITY_TIERS: { powerStrike: 'tier1', whirlwind: 'tier1', warcry: 'tier1', heal: 'tier2', dash: 'tier2', shieldWall: 'tier2', execute: 'tier2', berserk: 'ultimate', chronoEcho: 'ultimate' }, // Ability-specific frequency offsets for unique sounds ABILITY_OFFSETS: { powerStrike: 0, whirlwind: 50, warcry: 100, heal: 0, dash: 40, shieldWall: 80, execute: 120, berserk: 0, chronoEcho: 60 } }; function initAbilityReadyTracking() { // Initialize all abilities as ready (not on cooldown) - v8.08: forEach to for loop const keys = Object.keys(abilityState); for (let i = 0; i < keys.length; i++) { ABILITY_READY_AUDIO.previousCooldownState[keys[i]] = false; } } function playAbilityReadyCue(abilityKey) { // v7.28: Use shared AudioContext const audioCtx = getSharedAudioContext(); if (!audioCtx) return; const tier = ABILITY_READY_AUDIO.ABILITY_TIERS[abilityKey] || 'tier1'; const profile = ABILITY_READY_AUDIO.AUDIO_PROFILES[tier]; const offset = ABILITY_READY_AUDIO.ABILITY_OFFSETS[abilityKey] || 0; try { const masterGain = audioCtx.createGain(); masterGain.gain.value = 0.2; masterGain.connect(audioCtx.destination); // Main tone const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = profile.type; osc.frequency.value = profile.baseFreq + offset; // Quick attack, smooth decay gain.gain.setValueAtTime(0, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0.8, audioCtx.currentTime + 0.02); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + profile.duration); osc.connect(gain); gain.connect(masterGain); osc.start(audioCtx.currentTime); osc.stop(audioCtx.currentTime + profile.duration); // Harmonic overtone for richer sound if (tier !== 'tier1') { const osc2 = audioCtx.createOscillator(); const gain2 = audioCtx.createGain(); osc2.type = 'sine'; osc2.frequency.value = (profile.baseFreq + offset) * 1.5; // Perfect fifth gain2.gain.setValueAtTime(0, audioCtx.currentTime); gain2.gain.linearRampToValueAtTime(0.3, audioCtx.currentTime + 0.02); gain2.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + profile.duration * 0.8); osc2.connect(gain2); gain2.connect(masterGain); osc2.start(audioCtx.currentTime); osc2.stop(audioCtx.currentTime + profile.duration); } // Ultimate abilities get an extra shimmer if (tier === 'ultimate') { for (let i = 0; i < 3; i++) { const shimmer = audioCtx.createOscillator(); const shimmerGain = audioCtx.createGain(); shimmer.type = 'sine'; shimmer.frequency.value = (profile.baseFreq + offset) * 2 + i * 100; const delay = i * 0.05; shimmerGain.gain.setValueAtTime(0, audioCtx.currentTime + delay); shimmerGain.gain.linearRampToValueAtTime(0.15, audioCtx.currentTime + delay + 0.02); shimmerGain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + delay + 0.15); shimmer.connect(shimmerGain); shimmerGain.connect(masterGain); shimmer.start(audioCtx.currentTime + delay); shimmer.stop(audioCtx.currentTime + delay + 0.2); } } } catch (e) { console.log('Ability ready audio error:', e); } } function checkAbilityReadyStates() { if (!ABILITY_READY_AUDIO.previousCooldownState) return; // v8.08: forEach to for loop const abilityKeys = Object.keys(abilityState); for (let i = 0; i < abilityKeys.length; i++) { const abilityKey = abilityKeys[i]; // Only check unlocked abilities if (!isAbilityUnlocked(abilityKey)) continue; const wasOnCooldown = ABILITY_READY_AUDIO.previousCooldownState[abilityKey]; const isNowReady = isAbilityReady(abilityKey); // Detect transition from cooldown → ready if (wasOnCooldown && isNowReady) { playAbilityReadyCue(abilityKey); // Visual pulse on the ability icon pulseAbilityReady(abilityKey); } // Update tracking state ABILITY_READY_AUDIO.previousCooldownState[abilityKey] = !isNowReady; } } function pulseAbilityReady(abilityKey) { // Find the ability slot and pulse it with v7.23 burst effect const abilityKeyMap = { powerStrike: 'q', whirlwind: 'e', warcry: 'r', heal: 't', dash: 'f', shieldWall: 'z', execute: 'x', berserk: 'c', chronoEcho: 'b' }; const key = abilityKeyMap[abilityKey]; if (!key) return; const slotEl = document.querySelector(`[data-ability="${key}"]`); if (slotEl) { // v7.23: Enhanced "ready pop" burst animation slotEl.classList.add('just-ready'); setTimeout(() => { slotEl.classList.remove('just-ready'); }, 350); // Slightly longer than animation duration } } // Add CSS animation for ability ready pulse (function addAbilityReadyStyles() { if (document.getElementById('ability-ready-styles')) return; const style = document.createElement('style'); style.id = 'ability-ready-styles'; style.textContent = ` @keyframes abilityReadyPulse { 0% { transform: scale(1); box-shadow: 0 0 5px rgba(255,255,255,0.5); } 50% { transform: scale(1.15); box-shadow: 0 0 20px rgba(255,255,255,0.9), 0 0 30px rgba(100,200,255,0.6); } 100% { transform: scale(1); box-shadow: 0 0 5px rgba(255,255,255,0.5); } } `; document.head.appendChild(style); })(); // ============================================ // v7.2: INPUT BUFFER SYSTEM (8-Strategy Consensus Round 1) // Queues ability inputs during cooldowns for responsive feel // ============================================ const INPUT_BUFFER = { WINDOW: 200, // ms - buffer window before ability comes off cooldown ATTACK_BUFFER: 150, // ms - buffer for attack inputs during hit-stop queue: [], maxQueueSize: 2, attackBuffered: false, attackBufferTime: 0, // Buffer an ability if within timing window of cooldown ending bufferAbility(abilityKey) { const remaining = getAbilityCooldownRemaining(abilityKey); // If within buffer window, queue it if (remaining > 0 && remaining <= this.WINDOW) { // Don't duplicate if (!this.queue.find(q => q.ability === abilityKey)) { if (this.queue.length >= this.maxQueueSize) { this.queue.shift(); // Remove oldest } this.queue.push({ ability: abilityKey, bufferedAt: performance.now(), executeAt: performance.now() + remaining }); // Visual feedback - ability icon pulses pulseAbilityBuffered(abilityKey); return true; } } return false; }, // Buffer attack during hit-stop bufferAttack() { if (performance.now() < hitStopUntil) { this.attackBuffered = true; this.attackBufferTime = performance.now(); return true; } return false; }, // Process buffered inputs (call in game loop) processQueue() { const now = performance.now(); // Process ability buffer this.queue = this.queue.filter(buffered => { // Expire old buffers if (now - buffered.bufferedAt > this.WINDOW + 100) { return false; } // Try to execute if ready if (now >= buffered.executeAt && isAbilityReady(buffered.ability)) { useAbility(buffered.ability); return false; // Remove from queue } return true; }); // Process attack buffer after hit-stop ends if (this.attackBuffered && now >= hitStopUntil) { if (now - this.attackBufferTime < this.ATTACK_BUFFER + 50) { this.attackBuffered = false; // Return true to signal attack should execute return 'attack'; } this.attackBuffered = false; } return null; } }; // v7.2: Visual feedback for buffered ability function pulseAbilityBuffered(abilityKey) { const keyMap = { powerStrike: 'q', whirlwind: 'e', warcry: 'r', heal: 't', dash: 'f', shieldWall: 'z', execute: 'x', berserk: 'c', chronoEcho: 'b' }; const slotKey = keyMap[abilityKey]; const slot = document.querySelector(`.ability-slot[data-key="${slotKey}"]`); if (slot) { slot.style.boxShadow = '0 0 15px #0ff, inset 0 0 10px rgba(0,255,255,0.3)'; slot.style.transform = 'scale(1.05)'; setTimeout(() => { slot.style.boxShadow = ''; slot.style.transform = ''; }, 150); } } // ============================================ // v6.42: CHRONO-ECHO COMBAT SYSTEM // Records player combat actions and replays them as ghost clones // v7.92: Pooled ghost geometries and materials to avoid 7+ allocations per ghost // ============================================ const chronoEchoSystem = { actionHistory: [], maxHistorySize: 20, recordingEnabled: true, activeGhosts: [], // v7.92: Pooled geometries for ghost mesh creation _ghostGeometryPool: null, // v7.92: Pooled materials for ghost mesh creation _ghostMaterialPool: null, // v7.92: Pre-allocated vectors for position recording _tempRecordPos: null, _tempTargetPos: null, // v7.92: Initialize pooled resources initPool() { if (!this._ghostGeometryPool) { this._ghostGeometryPool = { body: new THREE.BoxGeometry(0.8, 1.2, 0.6), head: new THREE.SphereGeometry(0.35, 8, 8), arm: new THREE.CylinderGeometry(0.1, 0.1, 0.7, 6), leg: new THREE.CylinderGeometry(0.12, 0.12, 0.8, 6), ring: new THREE.RingGeometry(0.8, 1.0, 16) }; } if (!this._ghostMaterialPool) { this._ghostMaterialPool = { body: new THREE.MeshStandardMaterial({ color: 0x00ffff, transparent: true, opacity: 0.4, emissive: 0x00aaff, emissiveIntensity: 0.8 }), head: new THREE.MeshStandardMaterial({ color: 0x00ffff, transparent: true, opacity: 0.4, emissive: 0x8844ff, emissiveIntensity: 0.8 }), limb: new THREE.MeshStandardMaterial({ color: 0x00ffff, transparent: true, opacity: 0.4, emissive: 0x00aaff, emissiveIntensity: 0.8 }), ring: new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.3, side: THREE.DoubleSide }) }; } if (!this._tempRecordPos) this._tempRecordPos = new THREE.Vector3(); if (!this._tempTargetPos) this._tempTargetPos = new THREE.Vector3(); }, recordAction(actionType, position, rotation, targetPos, damage) { if (!this.recordingEnabled || mode !== 'world') return; // v7.92: Use copy instead of clone to avoid allocations if (!this._tempRecordPos) this._tempRecordPos = new THREE.Vector3(); this._tempRecordPos.copy(position); const storedPos = new THREE.Vector3().copy(this._tempRecordPos); let storedTargetPos = null; if (targetPos) { if (!this._tempTargetPos) this._tempTargetPos = new THREE.Vector3(); storedTargetPos = new THREE.Vector3().copy(targetPos); } this.actionHistory.push({ type: actionType, position: storedPos, rotation: rotation, targetPos: storedTargetPos, damage: damage, timestamp: performance.now() }); if (this.actionHistory.length > this.maxHistorySize) this.actionHistory.shift(); }, createGhostMesh(position, rotation, delay, actionData) { if (!scene || !worldState.player) return null; // v7.92: Ensure pool is initialized this.initPool(); const ghostGroup = new THREE.Group(); // v7.92: Use pooled geometry and materials instead of creating new ones const ghostBody = new THREE.Mesh(this._ghostGeometryPool.body, this._ghostMaterialPool.body); ghostBody.position.y = 1; ghostGroup.add(ghostBody); const ghostHead = new THREE.Mesh(this._ghostGeometryPool.head, this._ghostMaterialPool.head); ghostHead.position.y = 1.9; ghostGroup.add(ghostHead); [-1, 1].forEach(side => { const arm = new THREE.Mesh(this._ghostGeometryPool.arm, this._ghostMaterialPool.limb); arm.position.set(side * 0.6, 1.3, 0); arm.rotation.z = -side * Math.PI / 4; ghostGroup.add(arm); }); [-0.2, 0.2].forEach(x => { const leg = new THREE.Mesh(this._ghostGeometryPool.leg, this._ghostMaterialPool.limb); leg.position.set(x, 0.3, 0); ghostGroup.add(leg); }); const ring = new THREE.Mesh(this._ghostGeometryPool.ring, this._ghostMaterialPool.ring); ring.rotation.x = -Math.PI / 2; ring.position.y = 0.1; ghostGroup.add(ring); ghostGroup.position.copy(position); ghostGroup.rotation.y = rotation; ghostGroup.userData = { isChronoGhost: true, spawnTime: performance.now(), delay, actionData, hasAttacked: false, lifetime: 3000 + delay, phase: 0 }; scene.add(ghostGroup); this.activeGhosts.push(ghostGroup); return ghostGroup; }, // v8.08: Pre-allocated spawn position vector for ghost spawning _spawnPosPool: null, getSpawnPosVec() { if (!this._spawnPosPool) this._spawnPosPool = new THREE.Vector3(); return this._spawnPosPool; }, spawnGhosts(count, baseDamage) { if (!worldState.player || this.actionHistory.length === 0) return; const p = worldState.player; const actions = this.actionHistory.slice(-count); // v8.08: forEach to for loop + pooled spawn position for (let i = 0; i < actions.length; i++) { const action = actions[i]; const delay = i * 400; const angle = (i / count) * Math.PI * 2; // Need new Vector3 since it's passed to setTimeout closure const spawnPos = new THREE.Vector3( p.position.x + Math.sin(angle) * 2, p.position.y, p.position.z + Math.cos(angle) * 2 ); setTimeout(() => { const ghost = this.createGhostMesh(spawnPos, p.rotation.y + angle, delay, { ...action, baseDamage }); if (ghost) { spawnFloater(spawnPos, '👻', '#00ffff'); if (particles) particles.emit(spawnPos, 15, 0x00ffff, { spread: 2, lifetime: 500 }); setTimeout(() => this.executeGhostAttack(ghost), 500 + delay); } }, delay); } if (AudioSystem?.penta) { AudioSystem.playGentle(AudioSystem.penta.G4, 0.3, 0.2); setTimeout(() => AudioSystem.playGentle(AudioSystem.penta.C5, 0.25, 0.15), 100); setTimeout(() => AudioSystem.playGentle(AudioSystem.penta.E5, 0.2, 0.1), 200); } }, // v8.03: Converted forEach to for loop for performance executeGhostAttack(ghost) { if (!ghost?.userData || ghost.userData.hasAttacked) return; if (!worldState.mobs?.length) return; ghost.userData.hasAttacked = true; const { actionData } = ghost.userData; const ability = COMBAT_ABILITIES.chronoEcho; // v7.77: Use distanceToSquared to eliminate sqrt calls let nearestMob = null, nearestDistSq = 64; // 8 * 8 const mobs = worldState.mobs; for (let i = 0, len = mobs.length; i < len; i++) { const mob = mobs[i]; const mobPos = mob.mesh?.position || mob.position; if (!mobPos) continue; const distSq = ghost.position.distanceToSquared(mobPos); if (distSq < nearestDistSq && mob.userData.hp > 0) { nearestDistSq = distSq; nearestMob = mob; } } if (nearestMob) { const damage = Math.floor(actionData.baseDamage * ability.damageMultiplier); nearestMob.userData.hp -= damage; const mobPos = nearestMob.mesh?.position || nearestMob.position; spawnFloater(mobPos, `👻 -${damage}`, '#00ffff'); // v7.96: Use GlobalVec3Pool.acquire() for startPos (persists through animation), temp() for dir calculation const ghostStartPos = GlobalVec3Pool.acquire().copy(ghost.position); const attackDir = GlobalVec3Pool.temp().copy(mobPos).sub(ghost.position).normalize(); this.animateGhostAttack(ghost, ghostStartPos, attackDir); if (particles) particles.emit(nearestMob.position, 10, 0x00ffff, { spread: 2, lifetime: 400 }); if (nearestMob.userData.hp <= 0) { setTimeout(() => { if (nearestMob.userData.hp <= 0) performAction?.(nearestMob); }, 100); } } ghost.userData.fadeStart = performance.now(); }, // v7.96: startPos is an acquired pooled vector, release when animation completes animateGhostAttack(ghost, startPos, dir) { const startTime = performance.now(); const animate = () => { if (!ghost.parent) { GlobalVec3Pool.release(startPos); // v7.96: Release pooled vector on early exit return; } const t = Math.min((performance.now() - startTime) / 200, 1); const phase = t < 0.5 ? (1 - Math.pow(1 - t, 3)) * 2 : 2 - (1 - Math.pow(1 - t, 3)) * 2; ghost.position.copy(startPos).addScaledVector(dir, phase * 1.5); ghost.traverse(c => { if (c.material?.emissiveIntensity !== undefined) c.material.emissiveIntensity = 0.8 + phase * 1.5; }); if (t < 1) { requestAnimationFrame(animate); } else { GlobalVec3Pool.release(startPos); // v7.96: Release pooled vector on completion } }; animate(); }, update(dt) { const now = performance.now(); this.activeGhosts = this.activeGhosts.filter(ghost => { if (!ghost.parent) return false; ghost.userData.phase += dt * 3; ghost.position.y = (ghost.userData.actionData?.position?.y || 0) + Math.sin(ghost.userData.phase) * 0.15; ghost.children.find(c => c.geometry?.type === 'RingGeometry')?.rotation && (ghost.children.find(c => c.geometry?.type === 'RingGeometry').rotation.z += dt * 2); const fade = ghost.userData.hasAttacked ? Math.max(0, 1 - (now - ghost.userData.fadeStart) / 1000) : 1; ghost.traverse(c => { if (c.material?.opacity !== undefined) c.material.opacity = (0.3 + Math.sin(ghost.userData.phase * 2) * 0.1) * fade; }); if (now - ghost.userData.spawnTime > ghost.userData.lifetime || fade <= 0) { if (particles) particles.emit(ghost.position, 8, 0x00ffff, { spread: 1.5, lifetime: 300 }); scene.remove(ghost); return false; } return true; }); }, clearGhosts() { this.activeGhosts.forEach(g => g.parent && scene.remove(g)); this.activeGhosts = []; }, clearHistory() { this.actionHistory = []; } }; function isAbilityUnlocked(abilityKey) { const ability = COMBAT_ABILITIES[abilityKey]; return gameData.skills.combat.level >= ability.unlockLevel; } function isAbilityReady(abilityKey) { const ability = COMBAT_ABILITIES[abilityKey]; return performance.now() - abilityState[abilityKey].lastUsed >= ability.cooldown; } function getAbilityCooldownRemaining(abilityKey) { const ability = COMBAT_ABILITIES[abilityKey]; const elapsed = performance.now() - abilityState[abilityKey].lastUsed; return Math.max(0, ability.cooldown - elapsed); } function useAbility(abilityKey) { if (!isAbilityUnlocked(abilityKey)) { showNotification(`${COMBAT_ABILITIES[abilityKey].name} unlocks at Combat Lv ${COMBAT_ABILITIES[abilityKey].unlockLevel}`, 'warning'); return false; } if (!isAbilityReady(abilityKey)) { // v7.2: Try to buffer the ability instead of rejecting (8-Strategy Consensus Round 1) if (typeof INPUT_BUFFER !== 'undefined') { INPUT_BUFFER.bufferAbility(abilityKey); } // v10.19: Add audio/visual/haptic feedback for cooldown rejection (8-Strategy Cycle 6 Consensus) const cooldownRemaining = getAbilityCooldownRemaining(abilityKey); const cooldownSec = (cooldownRemaining / 1000).toFixed(1); // Audio feedback - subtle error tone if (typeof UISoundSystem !== 'undefined') UISoundSystem.play('error'); // Haptic feedback - light pulse if (typeof MobileHaptics !== 'undefined') MobileHaptics.vibrate('error'); // Visual feedback - show remaining cooldown if (worldState?.player) { spawnFloater(worldState.player.position, `⏱️ ${cooldownSec}s`, '#888888'); } // Pulse the ability slot visually const abilitySlot = document.querySelector(`.ability-slot[data-ability="${abilityKey}"]`); if (abilitySlot) { abilitySlot.classList.add('cooldown-rejected'); setTimeout(() => abilitySlot.classList.remove('cooldown-rejected'), 200); } return false; } if (mode !== 'world' || !worldState.player) return false; // v6.6: Null safety check for mobs array (Agent 2 bug fix) if (!worldState.mobs) worldState.mobs = []; const ability = COMBAT_ABILITIES[abilityKey]; // v12.17: UNIFIED BATTERY - Check and drain power for ability if (robotEnergy.unifiedMode && typeof UnifiedBatterySystem !== 'undefined' && ability.powerCost) { // Apply Battery Core efficiency bonus (reduces power cost) const efficiencyMult = typeof BatteryCoreSystem !== 'undefined' ? BatteryCoreSystem.getEfficiencyMultiplier() : 1.0; const actualCost = Math.ceil(ability.powerCost * efficiencyMult); if (!UnifiedBatterySystem.hasPower(actualCost)) { showNotification(`⚡ Not enough power for ${ability.name}! (Need ${actualCost} PWR)`, 'warning'); if (worldState.player) { spawnFloater(worldState.player.position, '⚡ NO POWER!', '#ff8800'); } return false; } // Drain power UnifiedBatterySystem.drainPower(actualCost); // Track combat for regen delay robotEnergy.lastCombatTime = performance.now(); } // v12.17: Award Battery Core XP for ability use if (typeof BatteryCoreSystem !== 'undefined') { BatteryCoreSystem.awardXP(BatteryCoreSystem.XP_VALUES.useAbility, 'ability'); } // v12.19: Adaptive AI - track ability usage if (typeof AdaptiveAISystem !== 'undefined') { AdaptiveAISystem.recordEvent('ability_used', { ability: abilityKey }); } const p = worldState.player; const now = performance.now(); abilityState[abilityKey].lastUsed = now; // v5.0: Track ability usage for quests trackAbilityUsage(); // v7.69: Suggest ability synergies to teach combos (8-Agent Consensus Cycle 44) if (typeof AbilitySynergySystem !== 'undefined') { AbilitySynergySystem.suggestCombo(abilityKey); } // v6.80: Ability activation flash (8-Agent Consensus) const elementMap = { powerStrike: 'fire', whirlwind: 'ice', warcry: 'fire', heal: 'holy', dash: 'lightning', shieldWall: 'ice', execute: 'dark', berserk: 'fire', chronoEcho: 'lightning' }; showAbilityFlash(elementMap[abilityKey] || ''); updateMomentum(5); // v7.22: Play ability-specific sound signature (8-Strategy Consensus Round 3) if (typeof AbilitySoundSystem !== 'undefined') { AbilitySoundSystem.play(abilityKey); } // v10.19: Haptic feedback for ability activation (8-Strategy Cycle 6 Consensus) if (typeof MobileHaptics !== 'undefined') { const heavyAbilities = ['powerStrike', 'whirlwind', 'execute', 'berserk']; MobileHaptics.vibrate(heavyAbilities.includes(abilityKey) ? 'heavyTap' : 'tap'); } // v6.90: Trigger robot casting animation for ability triggerRobotAnimation(abilityKey); // v6.36: Track ability for daily challenges if (typeof dailyChallenges !== 'undefined' && dailyChallenges.progress) { dailyChallenges.updateProgress('ability'); } if (abilityKey === 'powerStrike') { // v9.3: Power Strike now works without enemies - can be used for environment/utility // Get player facing direction for the strike const strikeDir = new THREE.Vector3(0, 0, -1); strikeDir.applyQuaternion(p.quaternion); // v9.3: Create iconic visual effect regardless of enemies if (typeof createPowerStrikeEffect === 'function') { createPowerStrikeEffect(p.position, strikeDir); } // Find nearest mob and deal massive damage (if any) // v7.77: Use distanceToSquared to eliminate sqrt calls // v8.01: forEach to for loop conversion let nearestMob = null; let nearestDistSq = 25; // 5 * 5 - Range limit squared for (let i = 0, len = worldState.mobs.length; i < len; i++) { const mob = worldState.mobs[i]; // v6.6: Safe position access (Agent 2 bug fix) const mobPos = mob.mesh?.position || mob.position; if (!mobPos) continue; const distSq = mobPos.distanceToSquared(p.position); if (distSq < nearestDistSq) { nearestDistSq = distSq; nearestMob = mob; } } // Always trigger the core effects triggerHitStop(HIT_STOP_BOSS); screenShake(1.0); AudioSystem.hit(comboState.count || 0); // v7.32: 3D spatial audio for power strike (Cycle 5 Consensus) if (typeof SpatialAudioSystem !== 'undefined' && SpatialAudioSystem.ctx && nearestMob) { SpatialAudioSystem.playHit3D(nearestMob.position, getPlayerDamage() * ability.damageMultiplier); } // v7.29: Power Strike creates small crater at impact point if (typeof TerrainCombatIntegration !== 'undefined') { const impactPos = strikeDir.clone().multiplyScalar(2).add(p.position); TerrainCombatIntegration.onHeavyImpact(impactPos.x, impactPos.z, 60, 'power_strike'); } if (nearestMob) { const damage = Math.floor(getPlayerDamage() * ability.damageMultiplier); nearestMob.userData.hp -= damage; spawnFloater(nearestMob.position, `${ability.icon} POWER STRIKE! -${damage}`, '#ff4400'); if (particles) particles.emit(nearestMob.position, 25, 0xff4400, { spread: 4, lifetime: 800 }); // Check kill if (nearestMob.userData.hp <= 0) { performAction(nearestMob); } } else { // No enemy - show ability activated anyway spawnFloater(p.position, `${ability.icon} POWER STRIKE!`, '#ff4400'); if (particles) particles.emit(p.position, 25, 0xff4400, { spread: 4, lifetime: 800 }); } } else if (abilityKey === 'whirlwind') { // v9.3: Whirlwind now works without enemies - can be used for environment/utility // Create iconic tornado visual effect regardless of enemies if (typeof createWhirlwindEffect === 'function') { createWhirlwindEffect(p.position); } // AoE damage to all nearby mobs // v7.77: Use distanceToSquared to eliminate sqrt calls // v8.01: forEach to for loop conversion const radiusSq = ability.radius * ability.radius; let hitCount = 0; for (let i = 0, len = worldState.mobs.length; i < len; i++) { const mob = worldState.mobs[i]; // v6.6: Safe position access (Agent 2 bug fix) const mobPos = mob.mesh?.position || mob.position; if (!mobPos) continue; const distSq = mobPos.distanceToSquared(p.position); if (distSq < radiusSq) { const damage = Math.floor(getPlayerDamage() * ability.damageMultiplier); mob.userData.hp -= damage; spawnFloater(mob.position, `${ability.icon} -${damage}`, '#00ffff'); hitCount++; if (mob.userData.hp <= 0) { // Queue for death handling setTimeout(() => { if (mob.userData.hp <= 0) performAction(mob); }, 100); } } } // Always trigger core effects triggerHitStop(HIT_STOP_HEAVY); screenShake(0.8); if (particles) particles.emit(p.position, 40, 0x00ffff, { spread: ability.radius, lifetime: 600 }); AudioSystem.hit(); // v7.29: Whirlwind creates circular depression around player if (typeof TerrainDeformationSystem !== 'undefined') { TerrainDeformationSystem.createCrater(p.position.x, p.position.z, 4, 1.5, { source: 'whirlwind' }); } if (hitCount > 0) { spawnFloater(p.position, `${ability.icon} WHIRLWIND! x${hitCount}`, '#00ffff'); } else { // No enemies - show ability activated anyway spawnFloater(p.position, `${ability.icon} WHIRLWIND!`, '#00ffff'); } } else if (abilityKey === 'warcry') { // v9.3: Create iconic sonic boom effect if (typeof createWarcryEffect === 'function') { createWarcryEffect(p.position); } // Activate damage buff abilityState.warcry.activeUntil = now + ability.duration; spawnFloater(p.position, `${ability.icon} WAR CRY!`, '#ff8800'); showNotification(`+${Math.floor((ability.damageBoost - 1) * 100)}% damage for ${ability.duration / 1000}s!`, 'success'); if (particles) particles.emit(p.position, 30, 0xff8800, { spread: 6, lifetime: 1000 }); screenShake(0.6); AudioSystem.levelUp(); } // v4.9: Tier 2 Abilities else if (abilityKey === 'heal') { // v9.3: Create iconic sacred light effect if (typeof createHealEffect === 'function') { createHealEffect(p.position); } // Self heal // v8.26: Guard against undefined gameData.player if (!gameData?.player?.hp || !gameData?.player?.maxHp) return; const healAmt = Math.floor(gameData.player.maxHp * ability.healAmount); gameData.player.hp = Math.min(gameData.player.maxHp, gameData.player.hp + healAmt); spawnFloater(p.position, `${ability.icon} +${healAmt} HP`, '#00ff88'); showNotification(`Healed ${healAmt} HP!`, 'success'); if (particles) particles.emit(p.position, 20, 0x00ff88, { spread: 3, lifetime: 800 }); updateHealthUI(); AudioSystem.levelUp(); } else if (abilityKey === 'dash') { // v6.36: Track dash for daily challenges if (typeof dailyChallenges !== 'undefined' && dailyChallenges.progress) { dailyChallenges.updateProgress('dash'); } // v6.13: LEVIATHAN PULSE - Ancient tech combat dash that DESTROYS obstacles! // v7.95: Use GlobalVec3Pool.temp() to eliminate clone() allocations in hot path const dir = GlobalVec3Pool.tempAt(0).set(0, 0, -1); dir.applyQuaternion(p.quaternion); const startPos = GlobalVec3Pool.tempAt(1).copy(p.position); const dashDir = GlobalVec3Pool.tempAt(2).copy(dir).normalize(); // Note: endPos used for effect, copy to persist across async calls const endPos = GlobalVec3Pool.acquire().copy(p.position).add(GlobalVec3Pool.tempAt(3).copy(dir).multiplyScalar(ability.distance)); // ========================================== // LEVIATHAN PULSE VISUAL EFFECT // ========================================== createFusRoDahEffect(startPos, dashDir, ability.distance); // v7.95: Pre-allocate temp vectors for mob/obstacle iteration const _tempMobPos = GlobalVec3Pool.tempAt(4); const _tempToMob = GlobalVec3Pool.tempAt(5); const _tempPerp = GlobalVec3Pool.tempAt(6); const _tempKnockback = GlobalVec3Pool.tempAt(7); // v8.02: forEach to for loop conversion for combat hot path // Damage enemies in path let dashHits = 0; const dashHitboxSq = 9; // 3 * 3 - v7.98: Use squared distance for hitbox check const dashMobs = worldState.mobs; const dashMobsLen = dashMobs.length; for (let i = 0; i < dashMobsLen; i++) { const mob = dashMobs[i]; _tempMobPos.copy(mob.position); // Check if mob is roughly between start and end _tempToMob.copy(_tempMobPos).sub(startPos); const projection = _tempToMob.dot(dashDir); if (projection > 0 && projection < ability.distance) { // v7.98: Inline perpendicular distance squared to avoid distanceTo() call _tempPerp.copy(dashDir).multiplyScalar(projection); const dx = _tempToMob.x - _tempPerp.x; const dy = _tempToMob.y - _tempPerp.y; const dz = _tempToMob.z - _tempPerp.z; const perpDistSq = dx * dx + dy * dy + dz * dz; if (perpDistSq < dashHitboxSq) { // Wider hitbox for Leviathan Pulse const damage = Math.floor(getPlayerDamage() * ability.damageMultiplier); mob.userData.hp -= damage; spawnFloater(mob.position, `💨 -${damage}`, '#88ffff'); dashHits++; // Knockback mobs in the dash direction - v7.95: reuse temp vector _tempKnockback.copy(dashDir).multiplyScalar(8); mob.position.add(_tempKnockback); if (mob.userData.hp <= 0) { setTimeout(() => { if (mob.userData.hp <= 0) performAction(mob); }, 100); } } } } // ========================================== // DESTROY TREES AND ROCKS IN PATH // ========================================== let obstaclesDestroyed = 0; const obstaclesToRemove = []; // v8.02: forEach to for loop conversion for combat hot path if (worldState.interactables) { const obstacleHitboxSq = 12.25; // 3.5 * 3.5 - v7.98: Use squared distance const dashInteractables = worldState.interactables; const dashInteractablesLen = dashInteractables.length; for (let i = 0; i < dashInteractablesLen; i++) { const obj = dashInteractables[i]; if (!obj.parent) continue; if (obj.userData && (obj.userData.type === 'tree' || obj.userData.type === 'rock')) { // v7.95: Reuse temp vectors instead of clone() _tempMobPos.copy(obj.position); _tempToMob.copy(_tempMobPos).sub(startPos); const projection = _tempToMob.dot(dashDir); // Check if object is in the dash path if (projection > -1 && projection < ability.distance + 2) { // v7.98: Inline perpendicular distance squared to avoid distanceTo() call _tempPerp.copy(dashDir).multiplyScalar(projection); const dx = _tempToMob.x - _tempPerp.x; const dy = _tempToMob.y - _tempPerp.y; const dz = _tempToMob.z - _tempPerp.z; const perpDistSq = dx * dx + dy * dy + dz * dz; if (perpDistSq < obstacleHitboxSq) { // Wide destruction path obstaclesToRemove.push(obj); } } } } } // Remove obstacles with visual effects obstaclesToRemove.forEach(obj => { const objType = obj.userData.type; // v7.95: Use temp vector for particles position const objPos = GlobalVec3Pool.temp().copy(obj.position); // Spawn destruction particles if (particles) { const color = objType === 'tree' ? 0x228b22 : 0x888888; particles.emit(objPos, 25, color, { spread: 5, lifetime: 800 }); } // Spawn debris floater const icon = objType === 'tree' ? '🌲' : '🪨'; spawnFloater(objPos, `${icon} SHATTERED!`, objType === 'tree' ? '#228b22' : '#888888'); // Give small resource reward for destruction const resourceType = objType === 'tree' ? '🪵 Wood' : '🪨 Stone'; addToInventory(resourceType, 1); // Track stats if (objType === 'tree') { gameData.statistics.treesChopped = (gameData.statistics.treesChopped || 0) + 1; } // Remove from scene and array scene.remove(obj); obstaclesDestroyed++; }); // Filter out removed obstacles if (obstaclesToRemove.length > 0) { worldState.interactables = worldState.interactables.filter( x => !obstaclesToRemove.includes(x) ); } // ========================================== // v6.16: DASH CLEARS FOG // The thermal shockwave from the dash disperses fog // ========================================== if (currentWeather === 'fog' && scene.fog) { createFogClearingEffect(startPos, dashDir, ability.distance); } // v7.29: Dash creates a trench along the path if (typeof TerrainDeformationSystem !== 'undefined') { TerrainDeformationSystem.createTrench( startPos.x, startPos.z, endPos.x, endPos.z, 2, 1, { source: 'dash' } ); } // Move player p.position.copy(endPos); // Show results const totalHits = dashHits + obstaclesDestroyed; if (totalHits > 0) { let msg = ''; if (dashHits > 0 && obstaclesDestroyed > 0) { msg = `DASH! ${dashHits} enemies + ${obstaclesDestroyed} obstacles!`; } else if (dashHits > 0) { msg = `DASH! ${dashHits} enemies sent flying!`; } else { msg = `DASH! ${obstaclesDestroyed} obstacles pulverized!`; } showNotification(msg, 'success'); triggerHitStop(obstaclesDestroyed > 2 ? HIT_STOP_BOSS : HIT_STOP_LIGHT); screenShake(0.5 + obstaclesDestroyed * 0.2); } else { spawnFloater(p.position, `💨 DASH! 💨`, '#88ffff'); } if (particles) particles.emit(startPos, 20, 0x88ffff, { spread: 3, lifetime: 500 }); AudioSystem.hit(); } else if (abilityKey === 'shieldWall') { // v9.3: Create iconic hexagonal barrier effect if (typeof createShieldWallEffect === 'function') { createShieldWallEffect(p.position); } // Activate damage reduction buff abilityState.shieldWall.activeUntil = now + ability.duration; spawnFloater(p.position, `${ability.icon} SHIELD WALL!`, '#4488ff'); showNotification(`${Math.floor(ability.damageReduction * 100)}% damage reduction for ${ability.duration / 1000}s!`, 'success'); if (particles) particles.emit(p.position, 25, 0x4488ff, { spread: 4, lifetime: 1000 }); screenShake(0.4); AudioSystem.levelUp(); } else if (abilityKey === 'execute') { // v9.3: Execute now works without enemies - can be used for environment/utility // Get player facing direction for the slash const executeDir = new THREE.Vector3(0, 0, -1); executeDir.applyQuaternion(p.quaternion); // v9.3: Create iconic death mark effect regardless of enemies if (typeof createExecuteEffect === 'function') { createExecuteEffect(p.position, executeDir); } // v7.79: High damage to low HP enemies - distanceToSquared optimization // v8.03: Converted forEach to for loop for performance let target = null; let nearestDistSq = 36; // 6 * 6 const mobs = worldState.mobs; for (let i = 0, len = mobs.length; i < len; i++) { const mob = mobs[i]; const distSq = mob.position.distanceToSquared(p.position); const hpPercent = mob.userData.hp / mob.userData.maxHp; if (distSq < nearestDistSq && hpPercent <= ability.threshold) { nearestDistSq = distSq; target = mob; } } // Always trigger core effects triggerHitStop(HIT_STOP_BOSS); screenShake(1.2); AudioSystem.hit(); if (target) { const damage = Math.floor(getPlayerDamage() * ability.damageMultiplier); target.userData.hp -= damage; spawnFloater(target.position, `${ability.icon} EXECUTE! -${damage}`, '#ff0044'); if (particles) particles.emit(target.position, 35, 0xff0044, { spread: 5, lifetime: 1000 }); if (target.userData.hp <= 0) { performAction(target); // v7.34: Reset Execute cooldown on kill (Cycle 13 - Game Balance) // Creates risk/reward loop - skilled timing enables chain executions if (ability.cooldownResetOnKill) { abilityState.execute.lastUsed = 0; spawnFloater(p.position, '💀 EXECUTE RESET!', '#ff00ff'); showNotification('Execute cooldown reset!', 'success'); AudioSystem.levelUp(); // Victory chime for successful reset // v7.35: "Soul Reap" visual effect - particles from victim to player (Cycle 14 - Visual Polish) // Dramatic visual showing power being absorbed to reset cooldown if (particles) { // Magenta soul particles at kill location particles.emit(target.position, 25, 0xff00ff, { spread: 3, lifetime: 800 }); // v7.96: Use GlobalVec3Pool.temp() to eliminate clone() allocation const midpoint = GlobalVec3Pool.temp().copy(p.position).lerp(target.position, 0.5); // Secondary burst toward player particles.emit(midpoint, 15, 0xff0088, { spread: 2, lifetime: 600 }); } // Screen flash for dramatic impact if (typeof showImpactBorder === 'function') { showImpactBorder('execute-reset'); } // Bloom system flash if (typeof BloomSystem !== 'undefined' && BloomSystem.flash) { BloomSystem.flash(0.4, 200); } } } } else { // No low HP enemies - show ability activated anyway spawnFloater(p.position, `${ability.icon} EXECUTE!`, '#ff0044'); if (particles) particles.emit(p.position, 35, 0xff0044, { spread: 5, lifetime: 1000 }); } } else if (abilityKey === 'berserk') { // v9.3: Create iconic rage aura effect if (typeof createBerserkEffect === 'function') { createBerserkEffect(p.position); } // ULTIMATE: Massive damage and attack speed buff abilityState.berserk.activeUntil = now + ability.duration; spawnFloater(p.position, `${ability.icon} BERSERKER RAGE!`, '#ff4400'); showNotification(`BERSERK! +100% DMG, +50% Attack Speed for ${ability.duration / 1000}s!`, 'success'); screenShake(1.5); if (particles) particles.emit(p.position, 50, 0xff4400, { spread: 8, lifetime: 1500 }); AudioSystem.levelUp(); // v7.29: Berserk creates a massive ground slam crater if (typeof TerrainDeformationSystem !== 'undefined') { TerrainDeformationSystem.createCrater(p.position.x, p.position.z, 8, 3, { source: 'berserk_slam' }); // Also spawn fissures radiating outward for (let i = 0; i < 4; i++) { const angle = (i / 4) * Math.PI * 2; const length = 12; const endX = p.position.x + Math.cos(angle) * length; const endZ = p.position.z + Math.sin(angle) * length; TerrainDeformationSystem.createFissure(p.position.x, p.position.z, endX, endZ, 2, { source: 'berserk_fissure' }); } } } // v6.42: CHRONO-ECHO - Summon time-echoes that replay past attacks else if (abilityKey === 'chronoEcho') { // Check if we have recorded actions to replay if (chronoEchoSystem.actionHistory.length === 0) { showNotification('No combat history! Attack enemies first.', 'warning'); abilityState[abilityKey].lastUsed = 0; return false; } abilityState.chronoEcho.activeUntil = now + ability.duration; const baseDamage = getPlayerDamage(); const ghostCount = Math.min(ability.echoCount, chronoEchoSystem.actionHistory.length); // Spawn ghost echoes chronoEchoSystem.spawnGhosts(ghostCount, baseDamage); // Visual activation effect spawnFloater(p.position, `${ability.icon} CHRONO-ECHO!`, '#00ffff'); showNotification(`${ghostCount} time-echoes summoned!`, 'success'); triggerHitStop(HIT_STOP_HEAVY); screenShake(0.8); // Time distortion overlay effect // v7.82: Use cached DOM reference to avoid getElementById per ability const container = getUICache().gameContainer; if (container) { container.style.boxShadow = 'inset 0 0 100px rgba(0, 255, 255, 0.4)'; setTimeout(() => { container.style.boxShadow = ''; }, 500); } // Particles emanating from player if (particles) { particles.emit(p.position, 40, 0x00ffff, { spread: 6, lifetime: 1200 }); particles.emit(p.position, 20, 0x8844ff, { spread: 4, lifetime: 800 }); } AudioSystem.levelUp(); } updateAbilityUI(); return true; } function isWarcryActive() { return performance.now() < abilityState.warcry.activeUntil; } // v4.9: Check if Shield Wall is active function isShieldWallActive() { return performance.now() < abilityState.shieldWall.activeUntil; } // v4.9: Check if Berserk is active function isBerserkActive() { return performance.now() < abilityState.berserk.activeUntil; } // v6.42: Check if Chrono-Echo is active (ghosts are present) function isChronoEchoActive() { return performance.now() < abilityState.chronoEcho.activeUntil || (typeof chronoEchoSystem !== 'undefined' && chronoEchoSystem.activeGhosts.length > 0); } function startDodge() { if (dodgeState.active || performance.now() < dodgeState.cooldownEnd) return false; if (mode !== 'world' || !worldState.player) return false; const p = worldState.player; dodgeState.active = true; dodgeState.startTime = performance.now(); dodgeState.cooldownEnd = performance.now() + DODGE_CONFIG.COOLDOWN; dodgeState.iframesEnd = performance.now() + DODGE_CONFIG.IFRAMES; // Direction based on current input or facing dodgeState.direction.set(0, 0, 0); if (keys.w) dodgeState.direction.z -= 1; if (keys.s) dodgeState.direction.z += 1; if (keys.a) dodgeState.direction.x -= 1; if (keys.d) dodgeState.direction.x += 1; // Also check joystick if (dodgeState.direction.length() < 0.1 && joystickActive) { dodgeState.direction.set(joystickInput.x, 0, joystickInput.y); } // Default to backward if no input if (dodgeState.direction.length() < 0.1) { dodgeState.direction.set(-Math.sin(p.rotation.y), 0, -Math.cos(p.rotation.y)); } dodgeState.direction.normalize(); AudioSystem.dodge(); if (particles) particles.emit(p.position, 10, 0x88ffff, { spread: 2, lifetime: 300, gravity: 0 }); // v5.15: Trigger robot jump animation on dodge triggerRobotAnimation('jump'); // v6.9: Style meter bonus on dodge (Agent consensus) if (typeof updateStyleMeter === 'function') { updateStyleMeter('dodge'); } // v8.0: Pet celebrates successful dodges! (8-Agent Consensus Cycle 5) if (typeof triggerPetReaction === 'function') { triggerPetReaction('dodge'); } // v4.6: Check for parry opportunity checkParryTiming(); return true; } function updateDodge(dt) { if (!dodgeState.active) return; const elapsed = performance.now() - dodgeState.startTime; const progress = elapsed / DODGE_CONFIG.DURATION; if (progress < 1) { const eased = 1 - Math.pow(1 - progress, 3); const moveAmount = (1 - eased) * DODGE_CONFIG.DISTANCE * dt * 10; // v7.84: Use pre-allocated temp vector instead of clone() per frame during dodge dodgeState._tempMoveVec.copy(dodgeState.direction).multiplyScalar(moveAmount); worldState.player.position.add(dodgeState._tempMoveVec); } else { dodgeState.active = false; } } function isInvincible() { const now = performance.now(); // v12.26: Check both dodge i-frames AND spawn protection return now < dodgeState.iframesEnd || now < SPAWN_PROTECTION.endTime; } // v12.26: Check if currently in spawn protection (for visual feedback) function hasSpawnProtection() { return performance.now() < SPAWN_PROTECTION.endTime; } // v12.26: Grant spawn invincibility function grantSpawnProtection() { SPAWN_PROTECTION.endTime = performance.now() + SPAWN_PROTECTION.DURATION; console.log('[SPAWN] Spawn protection granted for', SPAWN_PROTECTION.DURATION / 1000, 'seconds'); } // v4.6: Check if dodge was timed for a parry // v8.01: forEach to for loop conversion function checkParryTiming() { if (!worldState || !worldState.mobs) return; const now = performance.now(); let parried = false; for (let i = 0, len = worldState.mobs.length; i < len; i++) { const mob = worldState.mobs[i]; if (mob.userData.telegraphing && !mob.userData.stunned) { const timeToAttack = mob.userData.telegraphEnd - now; // Check if dodge was in the parry window (last PARRY_CONFIG.WINDOW ms before attack) if (timeToAttack > 0 && timeToAttack <= PARRY_CONFIG.WINDOW) { // Perfect parry! mob.userData.stunned = true; mob.userData.stunEnd = now + PARRY_CONFIG.STUN_DURATION; mob.userData.telegraphing = false; // Visual feedback // v10.12: Added emissive check if (mob.material?.emissive) mob.material.emissive.setHex(0xffff00); // Yellow stun mob.scale.setScalar(1); spawnFloater(mob.position, '⚡ PARRY!', '#ffd700'); // v7.33: 3D Parry Shockwave Ring at contact point (8-Strategy Cycle 12 - Visual Polish) // High-skill mechanic deserves impactful 3D visual at enemy position if (typeof abilityEffects !== 'undefined' && scene) { const ringGeo = new THREE.RingGeometry(0.3, 0.8, 24); const ringMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, // Parry signature cyan transparent: true, opacity: 0.9, side: THREE.DoubleSide }); const parryRing = new THREE.Mesh(ringGeo, ringMat); parryRing.position.copy(mob.position); parryRing.position.y = 0.15; parryRing.rotation.x = -Math.PI / 2; parryRing.userData = { createdAt: performance.now(), lifetime: 400, type: 'shockRing', expandRate: 6 }; scene.add(parryRing); abilityEffects.push(parryRing); } parried = true; } } } if (parried) { // Grant crit window parryState.critWindowEnd = now + PARRY_CONFIG.CRIT_WINDOW; parryState.lastParryTime = now; // v6.9: Style meter bonus on parry (Agent consensus) if (typeof updateStyleMeter === 'function') { updateStyleMeter('parry'); } // Audio feedback - v7.32: Use unique parry sound (8-Strategy Cycle 11 Consensus) AudioSystem.parry(); // v7.42: Haptic feedback for parry success on mobile (Cycle 21 Audio/Feedback consensus) if (typeof MobileHaptics !== 'undefined') { MobileHaptics.vibrate('parry'); } // Also play spatial parry sound at mob position if (typeof SpatialAudioSystem !== 'undefined' && SpatialAudioSystem.ctx && worldState.mobs.length > 0) { const parriedMob = worldState.mobs.find(m => m.userData.stunned && m.userData.stunEnd > performance.now()); if (parriedMob) SpatialAudioSystem.playParry3D(parriedMob.position); } // Screen effect // v7.41: Parry impact border for screen-wide visual feedback (Cycle 20 Visual Polish) if (typeof showImpactBorder === 'function') { showImpactBorder('parry'); } screenShake(0.3); if (particles) particles.emit(worldState.player.position, 25, 0xffd700, { spread: 4, lifetime: 500 }); showNotification('PERFECT PARRY! Critical hits enabled!'); } } // v4.6: Check if in crit window from parry function isInCritWindow() { return performance.now() < parryState.critWindowEnd; } // --- ENGINE CORE --- const CONFIG = { GALAXY_SIZE: 3000, NUM_CIVS: 60, // v6.64: BALANCED HIGH-RES TERRAIN - 2x resolution, visible blocks but smoother // Original: 100 tiles at size 2 = 200 world units (blocky) // New: 200 tiles at size 1 = 200 world units (smooth but visible) // 40,000 tiles - 4x smoother than original while terrain remains visible WORLD_SIZE: 200, TILE_SIZE: 1.0, TERRAIN_SCALE: 2, // Scale factor for noise sampling (maintains same terrain pattern) PLAYER_MAX_HP: 100, MOB_DAMAGE: 5, AUTOSAVE_INTERVAL: 30000, // 30 seconds // New v4.0 constants MOB_AGGRO_RANGE: 15, MOB_ATTACK_RANGE: 2, MOB_ATTACK_COOLDOWN: 1500, INTERACTION_RANGE: 3.5, INTERACTION_COOLDOWN: 400, // ms between actions MOVEMENT_THRESHOLD: 0.5, SCREEN_SHAKE_INTENSITY: 0.5, SCREEN_SHAKE_DURATION: 150, // v6.84: Pre-computed squared distances for hot path optimizations (avoids sqrt) MOB_AGGRO_RANGE_SQ: 15 * 15, // 225 MOB_ATTACK_RANGE_SQ: 2 * 2, // 4 INTERACTION_RANGE_SQ: 3.5 * 3.5 // 12.25 }; // ============================================ // v6.54: STEAM DECK GAMEPAD SUPPORT SYSTEM // 8-Agent Consensus Implementation // Supports: Detection, Input Mapping, Haptics, Auto-Attack // ============================================ const SteamDeckManager = { // State connected: false, gamepad: null, isSteamDeck: false, deckModeEnabled: 'auto', // 'auto', 'on', 'off' autoAttackEnabled: false, autoRetaliateEnabled: true, // v9.2: Auto-retaliate when attacked vibrationEnabled: true, targetFPS: 60, lastInputMode: 'keyboard', // 'keyboard', 'gamepad' // Deadzone and timing DEADZONE: 0.15, POLL_RATE: 16, // ~60fps lastPoll: 0, // Button state tracking (for edge detection) prevButtons: new Array(17).fill(false), buttonJustPressed: new Array(17).fill(false), // Radial menu state radialMenuOpen: false, radialSelection: -1, // Button indices (Standard Gamepad mapping) BUTTONS: { A: 0, B: 1, X: 2, Y: 3, LB: 4, RB: 5, LT: 6, RT: 7, SELECT: 8, START: 9, L3: 10, R3: 11, DPAD_UP: 12, DPAD_DOWN: 13, DPAD_LEFT: 14, DPAD_RIGHT: 15, HOME: 16 }, // Axis indices AXES: { LEFT_X: 0, LEFT_Y: 1, RIGHT_X: 2, RIGHT_Y: 3 }, // Initialize the system init() { if (this._initialized) return; this._initialized = true; this.detectSteamDeck(); window.addEventListener('gamepadconnected', (e) => this.onGamepadConnected(e)); window.addEventListener('gamepaddisconnected', (e) => this.onGamepadDisconnected(e)); // Check for already-connected gamepads const gamepads = navigator.getGamepads(); for (const gp of gamepads) { if (gp) { this.onGamepadConnected({ gamepad: gp }); break; } } this.loadSettings(); // v6.55: Initialize analytics module if (typeof SteamDeckAnalytics !== 'undefined') { SteamDeckAnalytics.init(); } console.log('[SteamDeck] Manager initialized. Deck detected:', this.isSteamDeck); }, // Detect if running on Steam Deck detectSteamDeck() { const isSteamDeckResolution = window.screen.width === 1280 && window.screen.height === 800; const isLinux = navigator.userAgent.toLowerCase().includes('linux'); this.isSteamDeck = isSteamDeckResolution && isLinux; if (this.deckModeEnabled === 'auto' && this.isSteamDeck) { this.applyDeckMode(true); } return this.isSteamDeck; }, // Gamepad connected onGamepadConnected(e) { this.gamepad = e.gamepad; this.connected = true; const gpId = e.gamepad.id.toLowerCase(); if (gpId.includes('steam') || gpId.includes('valve')) { this.isSteamDeck = true; if (this.deckModeEnabled === 'auto') { this.applyDeckMode(true); } } const indicator = document.getElementById('gamepad-indicator'); const status = document.getElementById('gamepad-status'); if (indicator) { indicator.classList.add('connected'); indicator.classList.remove('disconnected'); } if (status) { status.textContent = this.isSteamDeck ? 'Steam Deck' : 'Controller'; } const touchControls = document.getElementById('touch-controls'); if (touchControls && this.isSteamDeck) { touchControls.style.display = 'none'; } this.lastInputMode = 'gamepad'; if (typeof showNotification === 'function') { showNotification('🎮 Controller connected!', 'success'); } // v6.55: Track gamepad connection in analytics if (typeof SteamDeckAnalytics !== 'undefined') { SteamDeckAnalytics.trackGamepadConnected(e.gamepad.id); } console.log('[SteamDeck] Gamepad connected:', e.gamepad.id); }, // Gamepad disconnected onGamepadDisconnected(e) { this.gamepad = null; this.connected = false; const indicator = document.getElementById('gamepad-indicator'); const status = document.getElementById('gamepad-status'); if (indicator) { indicator.classList.remove('connected'); indicator.classList.add('disconnected'); } if (status) { status.textContent = 'Disconnected'; } const touchControls = document.getElementById('touch-controls'); if (touchControls) { touchControls.style.display = ''; } setTimeout(() => { if (!this.connected && indicator) { indicator.classList.remove('disconnected'); } }, 3000); if (typeof showNotification === 'function') { showNotification('🎮 Controller disconnected', 'warning'); } }, // Main polling function - called from game loop // v10.4: FRAME-BASED GAMEPAD POLLING (8-Agent Consensus Cycle 5) // Removed 16ms throttle for immediate input response (~50% latency improvement) poll(time) { if (!this.connected) return; // REMOVED: if (time - this.lastPoll < this.POLL_RATE) return; const gamepads = navigator.getGamepads(); this.gamepad = gamepads[this.gamepad?.index || 0]; if (!this.gamepad) return; // Track button edge detection - now happens every frame for (let i = 0; i < this.gamepad.buttons.length && i < 17; i++) { const pressed = this.gamepad.buttons[i].pressed; this.buttonJustPressed[i] = pressed && !this.prevButtons[i]; this.prevButtons[i] = pressed; } // v10.4: Analog stick temporal smoothing (stabilize without lag) const ANALOG_SMOOTH = 0.15; if (!this._smoothedAxes) this._smoothedAxes = [0, 0, 0, 0]; for (let i = 0; i < 4 && i < this.gamepad.axes.length; i++) { this._smoothedAxes[i] = this._smoothedAxes[i] * (1 - ANALOG_SMOOTH) + this.gamepad.axes[i] * ANALOG_SMOOTH; } // Process input based on game mode if (mode === 'world') { this.processWorldInput(); } else if (mode === 'galaxy') { this.processGalaxyInput(); } if (this.gamepad.buttons.some(b => b.pressed) || Math.abs(this.gamepad.axes[0]) > this.DEADZONE || Math.abs(this.gamepad.axes[1]) > this.DEADZONE) { this.lastInputMode = 'gamepad'; } }, // Process world mode input processWorldInput() { if (!this.gamepad || !worldState?.player) return; const gp = this.gamepad; const B = this.BUTTONS; // === MOVEMENT (Left Stick) - v10.4: Use smoothed axes === const lx = this.applyDeadzone(this._smoothedAxes ? this._smoothedAxes[this.AXES.LEFT_X] : gp.axes[this.AXES.LEFT_X]); const ly = this.applyDeadzone(this._smoothedAxes ? this._smoothedAxes[this.AXES.LEFT_Y] : gp.axes[this.AXES.LEFT_Y]); keys.w = ly < -0.3; keys.s = ly > 0.3; keys.a = lx < -0.3; keys.d = lx > 0.3; // === CAMERA (Right Stick) - for radial menu === const rx = this.applyDeadzone(gp.axes[this.AXES.RIGHT_X]); const ry = this.applyDeadzone(gp.axes[this.AXES.RIGHT_Y]); // === RADIAL MENU (Hold LB) === if (gp.buttons[B.LB].pressed) { if (!this.radialMenuOpen) { this.openRadialMenu(); } if (Math.abs(rx) > 0.5 || Math.abs(ry) > 0.5) { const angle = Math.atan2(ry, rx); const segment = Math.round((angle + Math.PI) / (Math.PI / 4)) % 8; this.selectRadialSegment(segment); } } else if (this.radialMenuOpen) { this.closeRadialMenu(); } // A Button: Primary action / Interact if (this.buttonJustPressed[B.A]) { if (typeof performAction === 'function' && worldState.target) { performAction(worldState.target); } } // B Button: Dodge if (this.buttonJustPressed[B.B]) { if (typeof startDodge === 'function') { startDodge(); } } // X Button: Whirlwind (E ability) if (this.buttonJustPressed[B.X]) { if (typeof useAbility === 'function') { useAbility('whirlwind'); } } // Y Button: Power Strike (Q ability) if (this.buttonJustPressed[B.Y]) { if (typeof useAbility === 'function') { useAbility('powerStrike'); } } // RB: Cycle targets if (this.buttonJustPressed[B.RB]) { this.cycleTarget(1); } // RT: Attack (when pressed) if (gp.buttons[B.RT].pressed && gp.buttons[B.RT].value > 0.5) { if (typeof performAction === 'function' && worldState.target) { performAction(worldState.target); } } // L3 (Left stick click): Temporal Rewind if (gp.buttons[B.L3].pressed) { if (typeof temporalRewind !== 'undefined' && !temporalRewind.isRewinding) { temporalRewind.startRewind(); } } else if (typeof temporalRewind !== 'undefined' && temporalRewind.isRewinding) { temporalRewind.stopRewind(); } // R3: Toggle auto-attack if (this.buttonJustPressed[B.R3]) { this.toggleAutoAttackInternal(); } // D-Pad Up: Quick heal if (this.buttonJustPressed[B.DPAD_UP]) { if (typeof useAbility === 'function') { useAbility('heal'); } } // D-Pad Down: Toggle inventory if (this.buttonJustPressed[B.DPAD_DOWN]) { const invPanel = document.getElementById('inventory-panel'); if (invPanel) { invPanel.style.display = invPanel.style.display === 'none' ? 'block' : 'none'; } } // Start: Menu/Settings if (this.buttonJustPressed[B.START]) { if (typeof toggleSettingsPanel === 'function') { toggleSettingsPanel(); } } // Select: Quick save if (this.buttonJustPressed[B.SELECT]) { if (typeof saveGameData === 'function') { saveGameData(); if (typeof showNotification === 'function') { showNotification('💾 Game saved!', 'success'); } this.vibrate('save'); } } }, // Process galaxy mode input processGalaxyInput() { if (!this.gamepad) return; const gp = this.gamepad; const B = this.BUTTONS; const lx = this.applyDeadzone(gp.axes[this.AXES.LEFT_X]); const ly = this.applyDeadzone(gp.axes[this.AXES.LEFT_Y]); if (camera && (Math.abs(lx) > 0.1 || Math.abs(ly) > 0.1)) { const orbitSpeed = 0.02; if (typeof galaxyRotation !== 'undefined') { galaxyRotation.y += lx * orbitSpeed; galaxyRotation.x = Math.max(-0.5, Math.min(0.5, galaxyRotation.x + ly * orbitSpeed)); } } const ry = this.applyDeadzone(gp.axes[this.AXES.RIGHT_Y]); if (Math.abs(ry) > 0.3 && camera) { const zoomSpeed = 5; camera.position.z = Math.max(50, Math.min(500, camera.position.z + ry * zoomSpeed)); } if (this.buttonJustPressed[B.RB]) { this.cyclePlanet(1); } if (this.buttonJustPressed[B.LB]) { this.cyclePlanet(-1); } if (this.buttonJustPressed[B.A]) { if (typeof selectedCiv !== 'undefined' && selectedCiv) { if (typeof enterWorld === 'function') { enterWorld(selectedCiv); } } } if (this.buttonJustPressed[B.B]) { const modals = document.querySelectorAll('.modal-overlay'); modals.forEach(m => { if (m.style.display !== 'none') { m.style.display = 'none'; } }); } if (this.buttonJustPressed[B.START]) { if (typeof toggleSettingsPanel === 'function') { toggleSettingsPanel(); } } }, // Cycle through targets cycleTarget(direction) { if (!worldState?.mobs || worldState.mobs.length === 0) return; const currentIdx = worldState.target ? worldState.mobs.indexOf(worldState.target) : -1; let nextIdx = currentIdx + direction; if (nextIdx >= worldState.mobs.length) nextIdx = 0; if (nextIdx < 0) nextIdx = worldState.mobs.length - 1; worldState.target = worldState.mobs[nextIdx]; this.vibrate('select'); }, // Cycle through planets in galaxy view cyclePlanet(direction) { if (typeof civilizations === 'undefined' || civilizations.length === 0) return; const currentIdx = typeof selectedCivIndex !== 'undefined' ? selectedCivIndex : 0; let nextIdx = currentIdx + direction; if (nextIdx >= civilizations.length) nextIdx = 0; if (nextIdx < 0) nextIdx = civilizations.length - 1; selectedCivIndex = nextIdx; selectedCiv = civilizations[nextIdx]; this.vibrate('select'); }, // Radial menu functions openRadialMenu() { this.radialMenuOpen = true; const menu = document.getElementById('radial-menu'); if (menu) menu.classList.add('active'); }, closeRadialMenu() { this.radialMenuOpen = false; const menu = document.getElementById('radial-menu'); if (menu) menu.classList.remove('active'); if (this.radialSelection >= 0) { const abilities = ['powerStrike', 'execute', 'whirlwind', 'berserk', 'warcry', 'dash', 'shield', 'heal']; const ability = abilities[this.radialSelection]; if (ability && typeof useAbility === 'function') { useAbility(ability); this.vibrate('ability'); } } this.radialSelection = -1; }, selectRadialSegment(segment) { if (segment === this.radialSelection) return; this.radialSelection = segment; const segments = document.querySelectorAll('.radial-segment'); segments.forEach((seg, i) => { seg.classList.toggle('selected', i === segment); }); this.vibrate('select'); }, // Apply deadzone to axis value applyDeadzone(value) { if (Math.abs(value) < this.DEADZONE) return 0; const sign = value > 0 ? 1 : -1; return sign * (Math.abs(value) - this.DEADZONE) / (1 - this.DEADZONE); }, // Haptic feedback vibrate(type) { if (!this.vibrationEnabled || !this.gamepad?.vibrationActuator) return; const patterns = { hit: { duration: 100, weakMagnitude: 0.3, strongMagnitude: 0.6 }, damage: { duration: 200, weakMagnitude: 0.8, strongMagnitude: 1.0 }, ability: { duration: 150, weakMagnitude: 0.4, strongMagnitude: 0.5 }, select: { duration: 50, weakMagnitude: 0.2, strongMagnitude: 0.1 }, save: { duration: 100, weakMagnitude: 0.3, strongMagnitude: 0.3 }, levelUp: { duration: 400, weakMagnitude: 0.5, strongMagnitude: 0.8 }, lowHealth: { duration: 300, weakMagnitude: 0.6, strongMagnitude: 0.4 } }; const pattern = patterns[type] || patterns.select; try { this.gamepad.vibrationActuator.playEffect('dual-rumble', { startDelay: 0, duration: pattern.duration, weakMagnitude: pattern.weakMagnitude, strongMagnitude: pattern.strongMagnitude }); } catch (e) { /* Vibration not supported */ } }, toggleAutoAttackInternal() { this.autoAttackEnabled = !this.autoAttackEnabled; const btn = document.getElementById('autoattack-toggle'); if (btn) { btn.textContent = this.autoAttackEnabled ? 'ON' : 'OFF'; btn.classList.toggle('active', this.autoAttackEnabled); } if (typeof showNotification === 'function') { showNotification(`Auto-Attack: ${this.autoAttackEnabled ? 'ON' : 'OFF'}`, 'info'); } this.saveSettings(); }, applyDeckMode(enabled) { if (enabled) { if (typeof particles !== 'undefined' && particles.maxParticles) { particles.maxParticles = Math.min(particles.maxParticles, 150); } this.targetFPS = 40; document.body.classList.add('deck-mode'); // v6.55: Track Deck Mode in analytics if (typeof SteamDeckAnalytics !== 'undefined') { SteamDeckAnalytics.trackDeckModeActive(true); } console.log('[SteamDeck] Deck Mode enabled'); } else { document.body.classList.remove('deck-mode'); this.targetFPS = 60; // v6.55: Track Deck Mode disabled if (typeof SteamDeckAnalytics !== 'undefined') { SteamDeckAnalytics.trackDeckModeActive(false); } console.log('[SteamDeck] Deck Mode disabled'); } }, // v7.77: Optimized with distanceToSquared to eliminate sqrt calls // v8.03: Converted forEach to for loop for performance updateAutoAttack() { if (!this.autoAttackEnabled || !worldState?.player || !worldState?.mobs) return; let nearestMob = null; const maxRange = CONFIG.MOB_ATTACK_RANGE * 1.5; let nearestDistSq = maxRange * maxRange; const mobs = worldState.mobs; for (let i = 0, len = mobs.length; i < len; i++) { const mob = mobs[i]; if (mob.userData.hp <= 0) continue; const distSq = mob.position.distanceToSquared(worldState.player.position); if (distSq < nearestDistSq) { nearestDistSq = distSq; nearestMob = mob; } } // v12.21: Also check Enemy Hero as potential auto-attack target if (typeof EnemyHeroSystem !== 'undefined' && EnemyHeroSystem.isAlive()) { const heroPos = EnemyHeroSystem.getPosition(); const heroState = EnemyHeroSystem.getState(); if (heroPos && heroState.mesh) { const heroDistSq = heroPos.distanceToSquared(worldState.player.position); if (heroDistSq < nearestDistSq) { nearestDistSq = heroDistSq; nearestMob = heroState.mesh; } } } if (nearestMob && typeof performAction === 'function') { worldState.target = nearestMob; performAction(nearestMob); } }, saveSettings() { const settings = { deckModeEnabled: this.deckModeEnabled, autoAttackEnabled: this.autoAttackEnabled, autoRetaliateEnabled: this.autoRetaliateEnabled, // v9.2 vibrationEnabled: this.vibrationEnabled, targetFPS: this.targetFPS }; localStorage.setItem('steamDeckSettings', JSON.stringify(settings)); }, // v8.0: Now using SafeJSON utility (8-Strategy Consensus Cycle 3) loadSettings() { const settings = SafeJSON.fromLocalStorage('steamDeckSettings', null); if (settings) { this.deckModeEnabled = settings.deckModeEnabled || 'auto'; this.autoAttackEnabled = settings.autoAttackEnabled || false; this.autoRetaliateEnabled = settings.autoRetaliateEnabled !== false; // v9.2: Default ON this.vibrationEnabled = settings.vibrationEnabled !== false; this.targetFPS = settings.targetFPS || 60; this.updateSettingsUI(); } }, updateSettingsUI() { const deckBtn = document.getElementById('deckmode-toggle'); const autoBtn = document.getElementById('autoattack-toggle'); const retaliateBtn = document.getElementById('autoretaliate-toggle'); // v9.2 const vibBtn = document.getElementById('vibration-toggle'); const fpsSelect = document.getElementById('target-fps'); if (deckBtn) deckBtn.textContent = this.deckModeEnabled.toUpperCase(); if (autoBtn) autoBtn.textContent = this.autoAttackEnabled ? 'ON' : 'OFF'; // v9.2: Update auto-retaliate button if (retaliateBtn) { retaliateBtn.textContent = this.autoRetaliateEnabled ? 'ON' : 'OFF'; retaliateBtn.classList.toggle('active', this.autoRetaliateEnabled); } if (vibBtn) vibBtn.textContent = this.vibrationEnabled ? 'ON' : 'OFF'; if (fpsSelect) fpsSelect.value = this.targetFPS.toString(); } }; // v6.77: PLANET NAVIGATOR - Quick switching between planets in galaxy view // 8-strategy consensus: Arrow buttons + pagination dots + keyboard support const PlanetNavigator = { visible: false, dotsWindow: 9, // Max dots to show at once // Initialize and show navigator when in galaxy mode with planets show() { if (typeof civilizations === 'undefined' || civilizations.length === 0) return; const nav = document.getElementById('planet-navigator'); if (!nav) return; this.visible = true; nav.classList.add('visible'); this.update(); }, // Hide navigator hide() { const nav = document.getElementById('planet-navigator'); if (nav) { nav.classList.remove('visible'); } this.visible = false; }, // Navigate to previous planet prev() { if (typeof civilizations === 'undefined' || civilizations.length === 0) return; // Find valid planets (not destroyed, not escaped) const validPlanets = this.getValidPlanets(); if (validPlanets.length === 0) return; const currentIdx = typeof selectedCivIndex !== 'undefined' ? selectedCivIndex : 0; let searchIdx = currentIdx - 1; if (searchIdx < 0) searchIdx = civilizations.length - 1; // Find previous valid planet while (searchIdx !== currentIdx) { if (!civilizations[searchIdx]?.orbital?.destroyed && !civilizations[searchIdx]?.orbital?.escaped) { break; } searchIdx--; if (searchIdx < 0) searchIdx = civilizations.length - 1; } this.goToIndex(searchIdx); }, // Navigate to next planet next() { if (typeof civilizations === 'undefined' || civilizations.length === 0) return; const validPlanets = this.getValidPlanets(); if (validPlanets.length === 0) return; const currentIdx = typeof selectedCivIndex !== 'undefined' ? selectedCivIndex : 0; let searchIdx = currentIdx + 1; if (searchIdx >= civilizations.length) searchIdx = 0; // Find next valid planet while (searchIdx !== currentIdx) { if (!civilizations[searchIdx]?.orbital?.destroyed && !civilizations[searchIdx]?.orbital?.escaped) { break; } searchIdx++; if (searchIdx >= civilizations.length) searchIdx = 0; } this.goToIndex(searchIdx); }, // Go to specific planet index goToIndex(idx) { if (typeof civilizations === 'undefined' || idx < 0 || idx >= civilizations.length) return; selectedCivIndex = idx; selectedCiv = civilizations[idx]; // Play selection sound if (typeof AudioSystem !== 'undefined' && AudioSystem.select) { AudioSystem.select(); } // Smooth camera transition (if in orbit mode during planet approach) if (typeof planetApproachState !== 'undefined' && planetApproachState.active) { planetApproachState.targetCiv = selectedCiv; } this.update(); console.log('[PlanetNav] Switched to planet:', selectedCiv?.name, 'index:', idx); }, // Get valid (non-destroyed, non-escaped) planets getValidPlanets() { if (typeof civilizations === 'undefined') return []; return civilizations.filter(c => !c?.orbital?.destroyed && !c?.orbital?.escaped); }, // Update the navigator UI update() { if (!this.visible || typeof civilizations === 'undefined') return; const validPlanets = this.getValidPlanets(); const currentIdx = typeof selectedCivIndex !== 'undefined' ? selectedCivIndex : 0; const currentPlanet = civilizations[currentIdx]; // v8.32: Use DOMCache.get() for planet navigator elements const countEl = DOMCache.get('planet-nav-count'); const nameEl = DOMCache.get('planet-nav-name'); const biomeEl = DOMCache.get('planet-nav-biome'); if (countEl) { const validIdx = validPlanets.indexOf(currentPlanet); countEl.textContent = `${validIdx >= 0 ? validIdx + 1 : '?'}/${validPlanets.length}`; } if (nameEl) { nameEl.textContent = currentPlanet?.name || 'Unknown'; } if (biomeEl) { biomeEl.textContent = currentPlanet?.biomeName || 'Unknown'; } // Update pagination dots (windowed view for many planets) this.updateDots(validPlanets, currentIdx); }, // Update pagination dots with windowed view updateDots(validPlanets, currentIdx) { // v8.32: Use DOMCache.get() for dots container const dotsContainer = DOMCache.get('planet-nav-dots'); if (!dotsContainer) return; dotsContainer.innerHTML = ''; const total = validPlanets.length; const windowSize = Math.min(this.dotsWindow, total); const currentValidIdx = validPlanets.indexOf(civilizations[currentIdx]); // Calculate window start (center current planet in window) let windowStart = Math.max(0, currentValidIdx - Math.floor(windowSize / 2)); if (windowStart + windowSize > total) { windowStart = Math.max(0, total - windowSize); } // Add left ellipsis if needed if (windowStart > 0) { const ellipsis = document.createElement('span'); ellipsis.className = 'planet-nav-ellipsis'; ellipsis.textContent = '···'; dotsContainer.appendChild(ellipsis); } // Create dots for visible window for (let i = windowStart; i < windowStart + windowSize && i < total; i++) { const planet = validPlanets[i]; const dot = document.createElement('div'); dot.className = 'planet-nav-dot'; if (i === currentValidIdx) { dot.classList.add('active'); } if (planet?.orbital?.destroyed) { dot.classList.add('destroyed'); } // Store actual civilization index for click handler const civIdx = civilizations.indexOf(planet); dot.onclick = () => this.goToIndex(civIdx); dot.title = planet?.name || `Planet ${i + 1}`; dotsContainer.appendChild(dot); } // Add right ellipsis if needed if (windowStart + windowSize < total) { const ellipsis = document.createElement('span'); ellipsis.className = 'planet-nav-ellipsis'; ellipsis.textContent = '···'; dotsContainer.appendChild(ellipsis); } }, // Check if navigator should be visible based on game state checkVisibility() { // Only show in galaxy mode with planets available if (mode !== 'galaxy' || typeof civilizations === 'undefined' || civilizations.length === 0) { this.hide(); return; } // Show if we have planets and are zoomed in, have a selection, or in planet approach const isZoomedIn = camera && camera.position.z < 300; const hasSelection = typeof selectedCiv !== 'undefined' && selectedCiv; const inApproach = typeof planetApproachState !== 'undefined' && planetApproachState.active; if (isZoomedIn || hasSelection || inApproach) { this.show(); } else { this.hide(); } } }; // Make globally accessible window.PlanetNavigator = PlanetNavigator; // Settings panel toggle functions for Steam Deck function toggleDeckMode() { const modes = ['auto', 'on', 'off']; const currentIdx = modes.indexOf(SteamDeckManager.deckModeEnabled); SteamDeckManager.deckModeEnabled = modes[(currentIdx + 1) % modes.length]; const btn = document.getElementById('deckmode-toggle'); if (btn) btn.textContent = SteamDeckManager.deckModeEnabled.toUpperCase(); if (SteamDeckManager.deckModeEnabled === 'on') { SteamDeckManager.applyDeckMode(true); } else if (SteamDeckManager.deckModeEnabled === 'off') { SteamDeckManager.applyDeckMode(false); } else { SteamDeckManager.applyDeckMode(SteamDeckManager.isSteamDeck); } SteamDeckManager.saveSettings(); } function toggleAutoAttack() { SteamDeckManager.toggleAutoAttackInternal(); } function toggleVibration() { SteamDeckManager.vibrationEnabled = !SteamDeckManager.vibrationEnabled; const btn = document.getElementById('vibration-toggle'); if (btn) btn.textContent = SteamDeckManager.vibrationEnabled ? 'ON' : 'OFF'; SteamDeckManager.saveSettings(); } // v9.2: Toggle auto-retaliate (attack back when hit) function toggleAutoRetaliate() { SteamDeckManager.autoRetaliateEnabled = !SteamDeckManager.autoRetaliateEnabled; const btn = document.getElementById('autoretaliate-toggle'); if (btn) { btn.textContent = SteamDeckManager.autoRetaliateEnabled ? 'ON' : 'OFF'; btn.classList.toggle('active', SteamDeckManager.autoRetaliateEnabled); } if (typeof showNotification === 'function') { showNotification(`Auto-Retaliate: ${SteamDeckManager.autoRetaliateEnabled ? 'ON' : 'OFF'}`, 'info'); } SteamDeckManager.saveSettings(); } function setTargetFPS(fps) { SteamDeckManager.targetFPS = parseInt(fps) || 60; // v10.20: Update frame time for throttling (8-Strategy Cycle 7 Consensus) targetFrameTime = 1000 / SteamDeckManager.targetFPS; SteamDeckManager.saveSettings(); } // ============================================ // v6.55: STEAM DECK ANALYTICS MODULE // Privacy-Respecting Local-First Analytics // Tracks device usage for developer insights // ============================================ const SteamDeckAnalytics = { STORAGE_KEY: 'levi_steamdeck_analytics', BEACON_KEY: 'levi_analytics_beacon_consent', // Current session data session: { startTime: null, endTime: null, deviceType: 'unknown', gamepadUsed: false, deckModeActive: false, featuresUsed: new Set(), playtimeMs: 0 }, // Initialize analytics init() { this.session.startTime = Date.now(); this.detectDeviceType(); this.loadAndMerge(); // Track session end on page unload window.addEventListener('beforeunload', () => this.endSession()); window.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { this.saveSession(); } }); console.log('[Analytics] Initialized. Device:', this.session.deviceType); }, // Detect device type detectDeviceType() { const ua = navigator.userAgent.toLowerCase(); const width = window.screen.width; const height = window.screen.height; // Steam Deck detection if (width === 1280 && height === 800 && ua.includes('linux')) { this.session.deviceType = 'steam_deck'; } // Mobile detection else if (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(ua)) { this.session.deviceType = /ipad/i.test(ua) ? 'tablet' : 'mobile'; } // Desktop detection with browser else { if (ua.includes('edg/')) this.session.deviceType = 'desktop_edge'; else if (ua.includes('chrome')) this.session.deviceType = 'desktop_chrome'; else if (ua.includes('firefox')) this.session.deviceType = 'desktop_firefox'; else if (ua.includes('safari')) this.session.deviceType = 'desktop_safari'; else this.session.deviceType = 'desktop_other'; } }, // Track feature usage trackFeature(featureName) { this.session.featuresUsed.add(featureName); }, // Track gamepad connection trackGamepadConnected(gamepadId) { this.session.gamepadUsed = true; this.trackFeature('gamepad_connected'); // Check if it's a Steam Controller if (gamepadId && (gamepadId.toLowerCase().includes('steam') || gamepadId.toLowerCase().includes('valve'))) { this.trackFeature('steam_controller'); } }, // Track Deck Mode activation trackDeckModeActive(active) { this.session.deckModeActive = active; if (active) this.trackFeature('deck_mode_enabled'); }, // Get aggregated analytics data // v6.84: Added error handling for corrupted localStorage data getData() { const raw = localStorage.getItem(this.STORAGE_KEY); if (!raw) return this.getEmptyData(); try { return JSON.parse(raw); } catch (e) { console.warn('Analytics data corrupted, resetting:', e); return this.getEmptyData(); } }, // Empty data structure getEmptyData() { return { version: 1, firstSeen: Date.now(), lastSeen: Date.now(), totalSessions: 0, totalPlaytimeMs: 0, deviceBreakdown: {}, featureUsage: {}, gamepadSessions: 0, deckModeSessions: 0, steamDeckSessions: 0 }; }, // Load existing data and prepare for merge loadAndMerge() { // Data will be merged on session end }, // Save current session to aggregated data saveSession() { const data = this.getData(); const sessionDuration = Date.now() - this.session.startTime; // Update aggregates data.lastSeen = Date.now(); data.totalPlaytimeMs += sessionDuration; // Device breakdown const device = this.session.deviceType; data.deviceBreakdown[device] = (data.deviceBreakdown[device] || 0) + 1; // Feature usage for (const feature of this.session.featuresUsed) { data.featureUsage[feature] = (data.featureUsage[feature] || 0) + 1; } // Gamepad and Deck mode tracking if (this.session.gamepadUsed) data.gamepadSessions++; if (this.session.deckModeActive) data.deckModeSessions++; if (this.session.deviceType === 'steam_deck') data.steamDeckSessions++; localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data)); }, // End session (called on unload) endSession() { const data = this.getData(); data.totalSessions++; localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data)); this.saveSession(); }, // Get summary for display getSummary() { const data = this.getData(); const hours = Math.floor(data.totalPlaytimeMs / 3600000); const minutes = Math.floor((data.totalPlaytimeMs % 3600000) / 60000); return { totalSessions: data.totalSessions, playtime: `${hours}h ${minutes}m`, steamDeckUsage: data.steamDeckSessions, gamepadUsage: data.gamepadSessions, deckModeUsage: data.deckModeSessions, deviceBreakdown: data.deviceBreakdown, topFeatures: Object.entries(data.featureUsage) .sort((a, b) => b[1] - a[1]) .slice(0, 5) }; }, // Export analytics data (for user to share) exportData() { const data = this.getData(); const summary = this.getSummary(); const exportObj = { exportDate: new Date().toISOString(), gameVersion: '6.55', summary: summary, rawData: data, // Privacy: No PII, just aggregated stats privacyNote: 'This data contains no personal information, only aggregated gameplay statistics.' }; const blob = new Blob([JSON.stringify(exportObj, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `levi-analytics-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); return exportObj; }, // Clear all analytics data clearData() { localStorage.removeItem(this.STORAGE_KEY); localStorage.removeItem(this.BEACON_KEY); console.log('[Analytics] Data cleared'); }, // ===== OPTIONAL BEACON SYSTEM (Tier 2) ===== // User must explicitly opt-in for this beaconConsent: false, // Check if user has opted into beacon hasBeaconConsent() { return localStorage.getItem(this.BEACON_KEY) === 'true'; }, // Set beacon consent setBeaconConsent(consent) { this.beaconConsent = consent; localStorage.setItem(this.BEACON_KEY, consent ? 'true' : 'false'); console.log('[Analytics] Beacon consent:', consent); }, // Send anonymous beacon (only if opted in) // This would ping a simple endpoint with device type only // NOT IMPLEMENTED - placeholder for future opt-in analytics service sendBeacon() { if (!this.hasBeaconConsent()) return false; // Minimal anonymous data const beacon = { t: Date.now(), d: this.session.deviceType, g: this.session.gamepadUsed ? 1 : 0, v: '6.55' }; // Would send to analytics endpoint here // navigator.sendBeacon('https://your-analytics-endpoint.com/levi', JSON.stringify(beacon)); console.log('[Analytics] Beacon would send:', beacon); return true; } }; // v6.55: Analytics UI helper functions function exportAnalyticsData() { if (typeof SteamDeckAnalytics !== 'undefined') { SteamDeckAnalytics.exportData(); if (typeof showNotification === 'function') { showNotification('📊 Analytics exported!', 'success'); } } } function clearAnalyticsData() { if (confirm('Clear all analytics data? This cannot be undone.')) { if (typeof SteamDeckAnalytics !== 'undefined') { SteamDeckAnalytics.clearData(); updateAnalyticsUI(); if (typeof showNotification === 'function') { showNotification('🗑️ Analytics cleared', 'warning'); } } } } // v8.32: Cached DOM references for analytics UI (eliminates 4 getElementById calls per update) let _analyticsCache = null; function getAnalyticsCache() { if (!_analyticsCache) { _analyticsCache = { sessions: DOMCache.get('analytics-sessions'), playtime: DOMCache.get('analytics-playtime'), deck: DOMCache.get('analytics-deck'), gamepad: DOMCache.get('analytics-gamepad') }; } return _analyticsCache; } function updateAnalyticsUI() { if (typeof SteamDeckAnalytics === 'undefined') return; const summary = SteamDeckAnalytics.getSummary(); const cache = getAnalyticsCache(); if (cache.sessions) cache.sessions.textContent = summary.totalSessions; if (cache.playtime) cache.playtime.textContent = summary.playtime; if (cache.deck) cache.deck.textContent = `${summary.steamDeckUsage} sessions`; if (cache.gamepad) cache.gamepad.textContent = `${summary.gamepadUsage} sessions`; } // v4.7: Player state for status effects const playerState = { chilled: false, chilledEnd: 0, moveSpeedMult: 1.0, // v6.42: Lava damage tracking (8-agent consensus) lastLavaDamageTime: 0, inLava: false, // v10.20: Quicksand damage tracking (Desert biome) lastQuicksandDamageTime: 0, inQuicksand: false, quicksandDepth: 0 // 0 = not in, 1 = shallow, 2 = deep }; // v6.42: Lava Damage Configuration (8-agent consensus: 5 damage / 500ms = 10 DPS) const LAVA_DAMAGE_CONFIG = { TICK_RATE: 500, // ms between damage ticks (matches fire DoT) DAMAGE: 5, // damage per tick (dangerous but escapable) FLOATER_COLOR: '#ff4400', // orange-red matching Volcanic biome FLOATER_ICON: '🔥' }; // v10.20: Quicksand Damage Configuration - Desert biome hazard // Low terrain areas in Desert act like quicksand - the deeper you sink, the more danger // Terrain values typically range from ~0.25 to ~1.0 (low areas are lighter colored) const QUICKSAND_CONFIG = { TICK_RATE: 600, // ms between damage ticks (slightly slower than lava) SHALLOW_DAMAGE: 2, // damage when slightly in quicksand DEEP_DAMAGE: 8, // damage when deep in quicksand SHALLOW_THRESHOLD: 0.35, // terrain height below this = shallow quicksand DEEP_THRESHOLD: 0.2, // terrain height below this = deep quicksand (high damage) SLOWDOWN_FACTOR: 0.4, // movement speed multiplier in quicksand FLOATER_COLOR: '#c4a35a', // sandy brown color FLOATER_ICON: '🏜️', WARNING_ICON: '⚠️' }; // v6.1: SPATIAL HASH GRID - O(1) entity lookups instead of O(n) const SpatialGrid = { cellSize: 10, grid: new Map(), entityToCell: new Map(), // Get cell key from position getCellKey(x, z) { const cx = Math.floor(x / this.cellSize); const cz = Math.floor(z / this.cellSize); return `${cx},${cz}`; }, // Add entity to grid add(entity) { if (!entity || !entity.position) return; const key = this.getCellKey(entity.position.x, entity.position.z); if (!this.grid.has(key)) this.grid.set(key, new Set()); this.grid.get(key).add(entity); this.entityToCell.set(entity, key); }, // Remove entity from grid remove(entity) { const oldKey = this.entityToCell.get(entity); if (oldKey && this.grid.has(oldKey)) { this.grid.get(oldKey).delete(entity); if (this.grid.get(oldKey).size === 0) this.grid.delete(oldKey); } this.entityToCell.delete(entity); }, // Update entity position (call when entity moves) update(entity) { if (!entity || !entity.position) return; const newKey = this.getCellKey(entity.position.x, entity.position.z); const oldKey = this.entityToCell.get(entity); if (oldKey !== newKey) { this.remove(entity); this.add(entity); } }, // Get all entities within radius of position (optimized) getNearby(x, z, radius, filter = null) { const results = []; const cellRadius = Math.ceil(radius / this.cellSize); const cx = Math.floor(x / this.cellSize); const cz = Math.floor(z / this.cellSize); const radiusSq = radius * radius; for (let dx = -cellRadius; dx <= cellRadius; dx++) { for (let dz = -cellRadius; dz <= cellRadius; dz++) { const key = `${cx + dx},${cz + dz}`; const cell = this.grid.get(key); if (!cell) continue; for (const entity of cell) { if (!entity.position) continue; const distSq = (entity.position.x - x) ** 2 + (entity.position.z - z) ** 2; if (distSq <= radiusSq) { if (!filter || filter(entity)) { results.push({ entity, distSq }); } } } } } // Sort by distance results.sort((a, b) => a.distSq - b.distSq); return results.map(r => r.entity); }, // Clear entire grid clear() { this.grid.clear(); this.entityToCell.clear(); }, // Rebuild grid from entities array rebuild(entities) { this.clear(); entities.forEach(e => this.add(e)); } }; // v6.1: Frame budget system for consistent performance const FrameBudget = { targetMs: 16, // 60 FPS target lastFrameTime: 0, lowPriorityQueue: [], frameSkipCounter: 0, // Check if we have budget for low-priority updates hasBudget() { return performance.now() - this.lastFrameTime < this.targetMs * 0.7; }, // Queue low-priority work queueLowPriority(fn) { this.lowPriorityQueue.push(fn); }, // Process queued work if budget allows processQueue() { while (this.lowPriorityQueue.length > 0 && this.hasBudget()) { const fn = this.lowPriorityQueue.shift(); try { fn(); } catch (e) { console.warn('Low priority task failed:', e); } } }, // Start frame timing startFrame() { this.lastFrameTime = performance.now(); }, // Should we skip this low-priority update? shouldSkipLowPriority() { this.frameSkipCounter++; return this.frameSkipCounter % 3 !== 0; // Skip 2 out of 3 frames for low-priority } }; // --- PRE-ALLOCATED REUSABLE OBJECTS --- const _tempVec3A = new THREE.Vector3(); const _tempVec3B = new THREE.Vector3(); // v6.83: Pre-allocated colors for day/night cycle (eliminates 2 Color allocations per frame) const _dayColor = new THREE.Color(); const _nightColor = new THREE.Color(0x050510); // v6.84: DOM element cache for hot path updates (eliminates 5-10 getElementById calls per second) let _uiCache = null; function getUICache() { if (!_uiCache) { _uiCache = { cycleCount: document.getElementById('cycle-count'), civCount: document.getElementById('civ-count'), // v6.92: Live civilization count perfFps: document.getElementById('perf-fps'), perfEntities: document.getElementById('perf-entities'), perfMobs: document.getElementById('perf-mobs'), perfDraws: document.getElementById('perf-draws'), perfTris: document.getElementById('perf-tris'), shipHpFill: document.getElementById('ship-hp-fill'), shipHpText: document.getElementById('ship-hp-text'), companionHealth: document.getElementById('companion-health-container'), dotaHpFill: document.getElementById('dota-hp-fill'), dotaHpText: document.getElementById('dota-hp-text'), dotaManaFill: document.getElementById('dota-mana-fill'), dotaManaText: document.getElementById('dota-mana-text'), criticalHpOverlay: document.getElementById('critical-hp-overlay'), // v7.42: Cached discover galaxy button (Cycle 21 Performance consensus) discoverGalaxyBtn: document.getElementById('discover-galaxy-btn'), // v7.71: Cached unified HUD elements (eliminates 5 getElementById calls per frame) unifiedHpFill: document.getElementById('unified-hp-fill'), unifiedHpText: document.getElementById('unified-hp-text'), unifiedMpFill: document.getElementById('unified-mp-fill'), unifiedMpText: document.getElementById('unified-mp-text'), unifiedLevelBadge: document.getElementById('unified-level-badge'), // v7.71: Cached impact border for damage feedback effects impactBorder: document.getElementById('impact-border'), // v7.77: Cached damage overlay elements (eliminates 2-4 getElementById calls per hit) damageOverlay: document.getElementById('damage-overlay'), directionalDamage: document.getElementById('directional-damage'), victoryFlash: document.getElementById('victory-flash') || document.getElementById('kill-flash'), // v7.82: Cached game container for ability effect box-shadow overlays (eliminates 6+ getElementById calls per ability use) gameContainer: document.getElementById('game-container') }; } return _uiCache; } // v6.84: Invalidate cache when DOM might have changed (e.g., after mode switch) function invalidateUICache() { _uiCache = null; } // v6.63: Optimal ARPG camera - Diablo-style isometric follow // Height 18, distance 15 creates ~50° angle for good terrain visibility // Robot stays centered and prominent, terrain flows around it const _camOffset = new THREE.Vector3(0, 18, 15); const _camLookOffset = new THREE.Vector3(0, -1, -2); // Look slightly ahead of robot // v6.41: Pre-allocated camera target vectors (eliminates 4-10 Vector3 allocations per frame) const _tempCamTarget = new THREE.Vector3(); const _tempCamLook = new THREE.Vector3(); // v7.34: Pre-allocated objects for updatePlayerDotaBars billboard calculation (eliminates 5 allocations per frame) // 8-Strategy Cycle 13 Consensus - Performance P5 const _dotaBarsWorldPos = new THREE.Vector3(); const _dotaBarsDirToCamera = new THREE.Vector3(); const _dotaBarsPlayerQuat = new THREE.Quaternion(); const _dotaBarsPlayerEuler = new THREE.Euler(); // v7.71: Pre-allocated matrix for water animation (eliminates Matrix4 allocation every 2 frames) const _waterAnimMatrix = new THREE.Matrix4(); // v7.71: Pre-allocated vectors for screenShake directional bias (eliminates 3 Vector3 allocations per shake) const _shakeCamRight = new THREE.Vector3(); const _shakeCamUp = new THREE.Vector3(); const _shakeCamForward = new THREE.Vector3(); // v8.10: Pre-allocated vector for click-to-move direction (eliminates allocation in hot path) const _clickToMoveDir = new THREE.Vector3(); // --- SCREEN EFFECTS --- // v6.41: Enhanced trauma-based screen shake (Agent 5 consensus - smoother, more cinematic) // v7.29: Added directional bias (Cycle 2 Consensus - attacks shake toward impact direction) let shakeTrauma = 0; let shakeTime = 0; let originalCameraPos = null; let shakeDirectionBias = { x: 0, y: 0 }; // v7.29: Directional punch function screenShake(intensity = CONFIG.SCREEN_SHAKE_INTENSITY, direction = null) { // v4.6: Check settings if (gameData.settings && !gameData.settings.screenShakeEnabled) return; // v6.41: Trauma is additive but capped - stacking impacts feel more impactful shakeTrauma = Math.min(1.0, shakeTrauma + intensity * 0.4); if (!originalCameraPos) originalCameraPos = new THREE.Vector3(); // v7.29: Apply directional bias if direction provided (Cycle 2 Consensus) // v7.71: Use pre-allocated vectors to avoid GC pressure (eliminates 3 allocations per shake) if (direction && camera) { // Project attack direction onto camera plane for screen-space bias camera.matrix.extractBasis(_shakeCamRight, _shakeCamUp, _shakeCamForward); const biasX = direction.dot(_shakeCamRight) * intensity * 3; const biasY = direction.dot(_shakeCamUp) * intensity * 1.5; shakeDirectionBias.x = biasX; shakeDirectionBias.y = biasY; } } function updateScreenShake() { if (shakeTrauma > 0 && mode === 'world') { shakeTime += 0.15; // Controls shake frequency // v6.41: Trauma squared for exponential falloff (Vlambeer's "juice" technique) const shake = shakeTrauma * shakeTrauma; // v6.41: Layered sine waves for smooth, organic motion instead of random jitter let offsetX = Math.sin(shakeTime * 15.3) * Math.cos(shakeTime * 8.7) * shake * 1.8; let offsetY = Math.sin(shakeTime * 12.1) * Math.cos(shakeTime * 9.3) * shake * 1.2; // v7.29: Add directional bias - decays faster than main shake (Cycle 2 Consensus) offsetX += shakeDirectionBias.x * shake; offsetY += shakeDirectionBias.y * shake; // Decay directional bias faster than trauma for quick punch effect shakeDirectionBias.x *= 0.85; shakeDirectionBias.y *= 0.85; camera.position.x += offsetX; camera.position.y += offsetY; // v6.41: Exponential trauma decay feels more natural than linear shakeTrauma = Math.max(0, shakeTrauma - 0.025); } } // Damage flash overlay // v7.77: Uses cached DOM reference to avoid getElementById per hit function flashDamageOverlay() { const overlay = getUICache().damageOverlay; if (overlay) { overlay.style.opacity = '0.4'; setTimeout(() => overlay.style.opacity = '0', 150); } } // v6.7: Directional damage indicator (Agent consensus - Combat Juice) // Shows a red gradient from the direction of the attacker // v7.77: Uses cached DOM reference to avoid getElementById per hit function flashDirectionalDamage(attackerPos) { if (!worldState.player || !attackerPos) return; const overlay = getUICache().directionalDamage; if (!overlay) return; // Calculate angle from player to attacker const playerPos = worldState.player.position; const dx = attackerPos.x - playerPos.x; const dz = attackerPos.z - playerPos.z; // Convert to screen space (accounting for camera rotation) // Camera looks down from behind, so we need to adjust let angle = Math.atan2(dx, dz) * (180 / Math.PI); // Adjust based on camera angle if available if (camera) { angle -= camera.rotation.y * (180 / Math.PI); } // Create directional gradient: red coming from the direction of attack overlay.style.background = `linear-gradient(${angle}deg, rgba(255,0,0,0.6) 0%, transparent 40%)`; overlay.style.opacity = '0.8'; setTimeout(() => { overlay.style.opacity = '0'; }, 200); } // v6.12: Victory celebration flash (Renamed from Kill for family-friendly) // v7.77: Uses cached DOM reference to avoid getElementById per victory function flashVictoryCelebration(isBoss = false) { const flash = getUICache().victoryFlash; if (!flash) return; if (isBoss) { // Epic gold/white flash for boss victories flash.style.background = 'radial-gradient(ellipse at center, rgba(255,255,255,0.6) 0%, rgba(255,215,0,0.4) 30%, transparent 70%)'; flash.style.opacity = '1'; setTimeout(() => flash.style.opacity = '0', 300); } else { // Subtle white flash for regular victories flash.style.background = 'radial-gradient(ellipse at center, rgba(255,255,255,0.3) 0%, transparent 60%)'; flash.style.opacity = '1'; setTimeout(() => flash.style.opacity = '0', 150); } } // Backwards compatibility alias function flashKillCelebration(isBoss) { flashVictoryCelebration(isBoss); } // ============================================ // v6.32: CAMERA PUNCH + DIRECTIONAL HIT-STOP // 8-Agent Consensus Implementation // Adds satisfying directional "punch" toward impact point // with FOV zoom for visceral combat feedback // ============================================ const CAMERA_PUNCH_CONFIG = { BASE_INTENSITY: 0.8, // Base punch distance FOV_PUNCH: 8, // FOV decrease on hit (degrees) PUNCH_DURATION: 120, // ms for punch animation RECOVERY_SPEED: 0.15, // Lerp factor for recovery FINISHER_MULT: 2.5, // Multiplier for finisher moves CRIT_MULT: 1.8, // Multiplier for critical hits BOSS_MULT: 3.0 // Multiplier for boss impacts }; const cameraPunchState = { active: false, startTime: 0, targetDirection: new THREE.Vector3(), intensity: 0, fovPunch: 0, baseFov: 60, // Default camera FOV currentFovOffset: 0, punchOffset: new THREE.Vector3(), // v7.89: Pre-allocated direction vector for punch calculation _tempDirection: null }; // Trigger camera punch toward impact point function triggerCameraPunch(targetPos, options = {}) { if (!camera || !worldState.player) return; if (gameData.settings && !gameData.settings.screenShakeEnabled) return; if (mode !== 'world') return; const { isFinisher, isCrit, isBoss, isKill } = options; // v7.89: Use pooled direction vector if (!cameraPunchState._tempDirection) cameraPunchState._tempDirection = new THREE.Vector3(); cameraPunchState._tempDirection.subVectors(targetPos, camera.position).normalize(); // Calculate intensity based on hit type let intensity = CAMERA_PUNCH_CONFIG.BASE_INTENSITY; let fovPunch = CAMERA_PUNCH_CONFIG.FOV_PUNCH; if (isBoss) { intensity *= CAMERA_PUNCH_CONFIG.BOSS_MULT; fovPunch *= 2; } else if (isFinisher) { intensity *= CAMERA_PUNCH_CONFIG.FINISHER_MULT; fovPunch *= 1.5; } else if (isCrit) { intensity *= CAMERA_PUNCH_CONFIG.CRIT_MULT; fovPunch *= 1.3; } // Extra punch on kills if (isKill) { intensity *= 1.4; fovPunch *= 1.2; } // Set punch state cameraPunchState.active = true; cameraPunchState.startTime = performance.now(); cameraPunchState.targetDirection.copy(cameraPunchState._tempDirection); cameraPunchState.intensity = intensity; cameraPunchState.fovPunch = fovPunch; } // Update camera punch in render loop function updateCameraPunch() { if (!cameraPunchState.active || !camera) return; const elapsed = performance.now() - cameraPunchState.startTime; const duration = CAMERA_PUNCH_CONFIG.PUNCH_DURATION; if (elapsed < duration) { // Punch phase - quick acceleration toward target const t = elapsed / duration; // Ease out cubic for snappy feel const easeOut = 1 - Math.pow(1 - t, 3); // Then ease back in for return const punchCurve = t < 0.3 ? easeOut * 3.33 // Quick punch in : 1 - ((t - 0.3) / 0.7); // Smooth return // Apply directional offset const offsetMagnitude = cameraPunchState.intensity * punchCurve; cameraPunchState.punchOffset.copy(cameraPunchState.targetDirection) .multiplyScalar(offsetMagnitude); camera.position.add(cameraPunchState.punchOffset); // FOV punch - narrow on impact, widen on return const fovCurve = t < 0.2 ? (t / 0.2) // Quick narrow : 1 - ((t - 0.2) / 0.8); // Smooth return cameraPunchState.currentFovOffset = -cameraPunchState.fovPunch * fovCurve; camera.fov = cameraPunchState.baseFov + cameraPunchState.currentFovOffset; camera.updateProjectionMatrix(); } else { // Recovery phase - smoothly return to normal cameraPunchState.currentFovOffset *= (1 - CAMERA_PUNCH_CONFIG.RECOVERY_SPEED); if (Math.abs(cameraPunchState.currentFovOffset) > 0.1) { camera.fov = cameraPunchState.baseFov + cameraPunchState.currentFovOffset; camera.updateProjectionMatrix(); } else { // Fully recovered camera.fov = cameraPunchState.baseFov; camera.updateProjectionMatrix(); cameraPunchState.active = false; cameraPunchState.currentFovOffset = 0; } } } // ============================================ // v6.32: HYPERSPACE JUMP TUNNEL EFFECT // 8-Agent Consensus Implementation // Epic warp tunnel with streaking stars during transitions // ============================================ const hyperspaceTunnel = { canvas: null, ctx: null, active: false, stars: [], animFrame: null, startTime: 0, duration: 2500, // ms callback: null, exitCallback: null, // Initialize canvas init() { this.canvas = document.getElementById('hyperspace-tunnel'); if (!this.canvas) return false; this.ctx = this.canvas.getContext('2d'); this.resize(); window.addEventListener('resize', () => this.resize()); return true; }, resize() { if (!this.canvas) return; this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; }, // Create stars for the tunnel effect createStars(count = 400) { this.stars = []; const cx = this.canvas.width / 2; const cy = this.canvas.height / 2; for (let i = 0; i < count; i++) { // Random position in a circle around center const angle = Math.random() * Math.PI * 2; const dist = Math.random() * 0.3; // Start close to center this.stars.push({ x: cx + Math.cos(angle) * dist * cx, y: cy + Math.sin(angle) * dist * cy, z: Math.random() * 1500 + 500, // Depth speed: Math.random() * 0.5 + 0.5, color: this.getStarColor(), trail: [] }); } }, getStarColor() { const colors = [ '#ffffff', '#aaddff', '#88ccff', '#66bbff', '#44aaff', '#00ffff', '#88ffff', '#aaffff' ]; return colors[Math.floor(Math.random() * colors.length)]; }, // Start the hyperspace effect start(duration = 2500, onMidpoint = null, onComplete = null) { if (!this.canvas && !this.init()) return; if (this.active) return; this.active = true; this.duration = duration; this.callback = onMidpoint; this.exitCallback = onComplete; this.startTime = performance.now(); this.createStars(); // Fade in this.canvas.style.opacity = '1'; // Play warp sound AudioSystem.playGentle(AudioSystem.penta.C4, 0.8, 0.15); setTimeout(() => AudioSystem.playGentle(AudioSystem.penta.G4, 0.6, 0.12), 100); setTimeout(() => AudioSystem.playGentle(AudioSystem.penta.C5, 0.5, 0.1), 200); // Start animation this.animate(); }, animate() { if (!this.active) return; // v8.34: Skip animation when tab is hidden if (!isPageVisible) { this.animFrame = requestAnimationFrame(() => this.animate()); return; } const elapsed = performance.now() - this.startTime; const progress = elapsed / this.duration; // Clear canvas with slight trail effect this.ctx.fillStyle = 'rgba(0, 0, 10, 0.2)'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); const cx = this.canvas.width / 2; const cy = this.canvas.height / 2; // Calculate warp intensity (peaks in the middle) const warpCurve = progress < 0.5 ? Math.pow(progress * 2, 2) // Accelerate : Math.pow(2 - progress * 2, 2); // Decelerate const warpSpeed = 5 + warpCurve * 50; // Draw center glow const glowRadius = 50 + warpCurve * 100; const gradient = this.ctx.createRadialGradient(cx, cy, 0, cx, cy, glowRadius); gradient.addColorStop(0, `rgba(255, 255, 255, ${0.3 + warpCurve * 0.4})`); gradient.addColorStop(0.3, `rgba(100, 200, 255, ${0.2 + warpCurve * 0.3})`); gradient.addColorStop(1, 'transparent'); this.ctx.fillStyle = gradient; this.ctx.fillRect(cx - glowRadius, cy - glowRadius, glowRadius * 2, glowRadius * 2); // v8.17: forEach-to-for loop conversion for warp stars animation (hot path) const warpStars = this.stars; for (let si = 0, slen = warpStars.length; si < slen; si++) { const star = warpStars[si]; // Store previous position for trail star.trail.unshift({ x: star.x, y: star.y }); if (star.trail.length > 10 + warpCurve * 15) star.trail.pop(); // Move star toward viewer (decrease z) star.z -= warpSpeed * star.speed; // Reset star if it passes the viewer if (star.z <= 0) { const angle = Math.random() * Math.PI * 2; star.x = cx; star.y = cy; star.z = 1500 + Math.random() * 500; star.color = this.getStarColor(); star.trail = []; } // Project 3D position to 2D const perspective = 400 / star.z; const sx = cx + (star.x - cx) * perspective * 3; const sy = cy + (star.y - cy) * perspective * 3; // Draw trail if (star.trail.length > 2) { this.ctx.beginPath(); this.ctx.moveTo(sx, sy); for (let i = 0; i < star.trail.length; i++) { const tz = star.z + i * 15; const tp = 400 / tz; const tx = cx + (star.trail[i].x - cx) * tp * 3; const ty = cy + (star.trail[i].y - cy) * tp * 3; this.ctx.lineTo(tx, ty); } const alpha = Math.min(1, perspective * 2) * (0.5 + warpCurve * 0.5); this.ctx.strokeStyle = star.color.replace(')', `, ${alpha})`).replace('rgb', 'rgba'); this.ctx.lineWidth = 1 + perspective * 3; this.ctx.stroke(); } // Draw star point const size = Math.max(1, perspective * 4); this.ctx.fillStyle = star.color; this.ctx.beginPath(); this.ctx.arc(sx, sy, size, 0, Math.PI * 2); this.ctx.fill(); // Update star position for next frame star.x += (star.x - cx) * 0.02 * warpSpeed * 0.1; star.y += (star.y - cy) * 0.02 * warpSpeed * 0.1; } // Draw vignette const vignette = this.ctx.createRadialGradient( cx, cy, this.canvas.width * 0.3, cx, cy, this.canvas.width * 0.7 ); vignette.addColorStop(0, 'transparent'); vignette.addColorStop(1, 'rgba(0, 0, 20, 0.8)'); this.ctx.fillStyle = vignette; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); // Trigger midpoint callback if (progress >= 0.5 && this.callback) { this.callback(); this.callback = null; // Only call once } // End effect if (progress >= 1) { this.stop(); return; } this.animFrame = requestAnimationFrame(() => this.animate()); }, stop() { this.active = false; if (this.animFrame) { cancelAnimationFrame(this.animFrame); this.animFrame = null; } // Fade out if (this.canvas) { this.canvas.style.opacity = '0'; } // Clear after fade setTimeout(() => { if (this.ctx) { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); } }, 300); // Play arrival sound AudioSystem.playGentle(AudioSystem.penta.G4, 0.3, 0.15); setTimeout(() => AudioSystem.playGentle(AudioSystem.penta.C4, 0.4, 0.2), 100); // Call completion callback if (this.exitCallback) { this.exitCallback(); this.exitCallback = null; } } }; // Convenience function for hyperspace jump function triggerHyperspaceJump(duration = 2500, onMidpoint = null, onComplete = null) { hyperspaceTunnel.start(duration, onMidpoint, onComplete); } // ============================================ // v6.33: HEARTBEAT WORLD PULSE SYSTEM // 8-Agent Consensus Synaesthetic Effect // World visually pulses in sync with low-HP heartbeat // Creates tunnel vision, desaturation, and red vignette // ============================================ const heartbeatWorldPulse = { active: false, pulsePhase: 0, lastHpPercent: 1, // Initialize the visual callback init() { AudioSystem.heartbeatVisualCallback = (hpPercent) => { this.triggerPulse(hpPercent); }; }, // Trigger a single heartbeat pulse triggerPulse(hpPercent) { if (mode !== 'world') return; this.active = true; this.pulsePhase = 1; this.lastHpPercent = hpPercent; // Intensity scales inversely with HP (lower HP = stronger effect) const intensity = 1 - hpPercent; // Apply visual effects this.applyScreenPulse(intensity); this.applyWorldDimming(intensity); this.applyCameraContraction(intensity); // v7.40: Synchronized haptic pulse for mobile low-health warning (Cycle 19 Audio/Feedback) // Uses the existing lowHealth pattern that was defined but never used if (typeof MobileHaptics !== 'undefined') { MobileHaptics.vibrate('lowHealth'); } }, // Red vignette pulse synchronized with heartbeat // v7.82: Use cached DOM reference to avoid getElementById per pulse applyScreenPulse(intensity) { const overlay = getUICache().damageOverlay; if (!overlay) return; // Create pulsing red vignette const alpha = 0.15 + intensity * 0.25; overlay.style.background = `radial-gradient(ellipse at center, transparent 20%, rgba(80,0,0,${alpha}) 70%, rgba(120,0,0,${alpha * 1.3}) 100%)`; overlay.style.opacity = '1'; // Pulse out over 400ms setTimeout(() => { overlay.style.opacity = '0.5'; setTimeout(() => { overlay.style.opacity = '0'; }, 200); }, 200); }, // Dim distant objects during pulse (tunnel vision effect) applyWorldDimming(intensity) { if (!scene || !scene.fog) return; // Store original fog if not stored if (!this._originalFogFar) { this._originalFogFar = scene.fog.far; this._originalFogNear = scene.fog.near; } // Contract fog during pulse (tunnel vision) const fogContract = intensity * 0.3; scene.fog.far = this._originalFogFar * (1 - fogContract); scene.fog.near = this._originalFogNear * (1 - fogContract * 0.5); // Return to normal over 400ms setTimeout(() => { if (scene && scene.fog) { scene.fog.far = this._originalFogFar; scene.fog.near = this._originalFogNear; } }, 400); }, // Slight camera zoom/contract on heartbeat applyCameraContraction(intensity) { if (!camera) return; // Store base FOV const baseFov = cameraPunchState.baseFov || 60; // Brief FOV reduction (tunnel vision feeling) const fovReduction = intensity * 3; camera.fov = baseFov - fovReduction; camera.updateProjectionMatrix(); // Ease back to normal setTimeout(() => { camera.fov = baseFov - fovReduction * 0.5; camera.updateProjectionMatrix(); setTimeout(() => { camera.fov = baseFov; camera.updateProjectionMatrix(); }, 200); }, 200); }, // Reset all effects when HP recovers reset() { this.active = false; this.pulsePhase = 0; if (this._originalFogFar && scene && scene.fog) { scene.fog.far = this._originalFogFar; scene.fog.near = this._originalFogNear; } } }; // Initialize heartbeat world pulse when game starts setTimeout(() => heartbeatWorldPulse.init(), 1000); // ============================================ // v6.33: COMBO CHROMATIC CRESCENDO SYSTEM // 8-Agent Consensus Feature // Each combo hit shifts through color spectrum // Creates rainbow satisfaction feedback // ============================================ const comboChromaticSystem = { // Color progression through spectrum per combo colors: [ { r: 255, g: 60, b: 60 }, // Hit 1: Red { r: 255, g: 140, b: 0 }, // Hit 2: Orange { r: 255, g: 220, b: 0 }, // Hit 3: Yellow { r: 60, g: 255, b: 100 }, // Hit 4: Green { r: 0, g: 220, b: 255 } // Hit 5+: Cyan (finisher) ], // Get color for combo count getComboColor(comboCount) { const idx = Math.min(comboCount, this.colors.length - 1); return this.colors[idx]; }, // Get CSS color string getComboColorCSS(comboCount) { const c = this.getComboColor(comboCount); return `rgb(${c.r}, ${c.g}, ${c.b})`; }, // Get hex color for Three.js getComboColorHex(comboCount) { const c = this.getComboColor(comboCount); return (c.r << 16) | (c.g << 8) | c.b; }, // Apply aura glow effect to player applyPlayerAura(comboCount) { if (!worldState.player) return; const color = this.getComboColorHex(comboCount); const intensity = 0.3 + comboCount * 0.15; // Apply emissive glow to player mesh worldState.player.traverse(child => { if (child.material && child.material.emissive) { child.material.emissive.setHex(color); child.material.emissiveIntensity = intensity; } }); // Fade out over time setTimeout(() => { if (!worldState.player) return; worldState.player.traverse(child => { if (child.material && child.material.emissive) { child.material.emissiveIntensity *= 0.5; } }); }, 300); }, // Create chromatic flash overlay // v7.82: Use cached DOM reference to avoid getElementById per combo triggerChromaticFlash(comboCount) { const flash = getUICache().victoryFlash; if (!flash) return; const color = this.getComboColor(comboCount); const alpha = 0.15 + comboCount * 0.05; flash.style.background = `radial-gradient(ellipse at center, rgba(${color.r},${color.g},${color.b},${alpha}) 0%, transparent 60%)`; flash.style.opacity = '1'; setTimeout(() => flash.style.opacity = '0', 100); }, // Emit chromatic particles emitChromaticParticles(position, comboCount) { if (!particles) return; const color = this.getComboColorHex(comboCount); const count = 3 + comboCount * 2; particles.emit(position, count, color, { spread: 2 + comboCount * 0.5, lifetime: 400 + comboCount * 100, size: 0.15 + comboCount * 0.03 }); }, // Full combo effect package triggerComboEffect(comboCount, position) { this.applyPlayerAura(comboCount); this.triggerChromaticFlash(comboCount); if (position) { this.emitChromaticParticles(position, comboCount); } }, // Reset player aura when combo breaks resetAura() { if (!worldState.player) return; worldState.player.traverse(child => { if (child.material && child.material.emissive) { child.material.emissiveIntensity = 0; } }); } }; // ============================================ // v6.33: SYNAPTIC BASS DROP COMBAT // 8-Agent Consensus Feature // Dramatic kill satisfaction effect // ============================================ const synapticBassDrop = { // Trigger bass drop effect on kill trigger(position, isBoss = false) { if (mode !== 'world') return; // 1. Audio silence then bass thump this.audioEffect(isBoss); // 2. Screen compression this.screenCompression(isBoss); // 3. Radial shockwave particles this.shockwaveParticles(position, isBoss); // 4. Time freeze micro-hitstop this.microFreeze(isBoss); }, audioEffect(isBoss) { // Brief silence (50ms), then deep bass AudioSystem.masterVolume = 0; setTimeout(() => { AudioSystem.masterVolume = 0.2; // Deep bass thump AudioSystem.playGentle(AudioSystem.penta.C3 / 2, 0.5, isBoss ? 0.4 : 0.25); if (isBoss) { setTimeout(() => AudioSystem.playGentle(AudioSystem.penta.G3 / 2, 0.4, 0.2), 100); } }, 50); }, screenCompression(isBoss) { const container = document.getElementById('container'); if (!container) return; // Compress screen briefly const scale = isBoss ? 0.97 : 0.985; container.style.transform = `scale(${scale})`; container.style.transition = 'transform 0.05s ease-out'; // Expand back with bounce setTimeout(() => { container.style.transform = 'scale(1.01)'; setTimeout(() => { container.style.transform = 'scale(1)'; container.style.transition = ''; }, 100); }, 50); }, shockwaveParticles(position, isBoss) { if (!particles || !position) return; // Radial burst of white particles const count = isBoss ? 40 : 20; for (let i = 0; i < count; i++) { const angle = (i / count) * Math.PI * 2; const offset = new THREE.Vector3( Math.cos(angle) * 0.5, 0.1, Math.sin(angle) * 0.5 ); particles.emit( position.clone().add(offset), 1, 0xffffff, { spread: 0.5, lifetime: 300, size: 0.2 } ); } }, microFreeze(isBoss) { // Extended hit-stop for bass drop effect const duration = isBoss ? 120 : 60; triggerHitStop(duration); } }; // ============================================ // v6.33: FUTURE GHOST COMBAT TELEGRAPH // 8-Agent Consensus Feature // Shows premonition of player death when lethal attack incoming // "See your death to prevent it" // ============================================ const futureGhostTelegraph = { ghostOverlay: null, isShowing: false, lastWarningTime: 0, warningCooldown: 2000, // Don't spam warnings // Calculate if incoming attack would be lethal // v8.25: Added defensive guards wouldBeLethal(incomingDamage) { // v8.25: Guard against undefined gameData.player if (!gameData || !gameData.player) return false; const defense = typeof getPlayerDefense === 'function' ? getPlayerDefense() : 0; const actualDamage = Math.max(1, safeNumber(incomingDamage) - defense); return safeNumber(gameData.player.hp, 1) <= actualDamage; }, // Create ghost overlay element if needed ensureOverlay() { if (this.ghostOverlay) return; this.ghostOverlay = document.createElement('div'); this.ghostOverlay.id = 'ghost-telegraph'; this.ghostOverlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 9999; display: flex; align-items: center; justify-content: center; background: transparent; opacity: 0; transition: opacity 0.15s; `; this.ghostOverlay.innerHTML = `
💀
DODGE!
`; document.body.appendChild(this.ghostOverlay); }, // Show the ghost premonition warning showDeathPremonition(attackerPosition) { if (mode !== 'world') return; const now = Date.now(); if (now - this.lastWarningTime < this.warningCooldown) return; this.lastWarningTime = now; this.ensureOverlay(); this.isShowing = true; // Pulsing red vignette overlay // v7.82: Use cached DOM reference to avoid getElementById per warning const container = getUICache().gameContainer; if (container) { container.style.boxShadow = 'inset 0 0 150px rgba(255, 0, 0, 0.6)'; } // Flash the ghost this.ghostOverlay.style.opacity = '1'; this.ghostOverlay.style.background = 'radial-gradient(ellipse at center, rgba(0,0,0,0.5) 0%, transparent 70%)'; const skull = this.ghostOverlay.querySelector('.ghost-skull'); const text = this.ghostOverlay.querySelector('.ghost-text'); if (skull) { skull.style.color = 'rgba(255, 0, 0, 0.3)'; skull.style.transform = 'scale(1.2)'; skull.style.filter = 'blur(0px)'; } if (text) { text.style.color = 'rgba(255, 50, 50, 0.9)'; } // Play warning sound - dramatic low tone if (AudioSystem && AudioSystem.penta) { AudioSystem.playGentle(AudioSystem.penta.C3 / 4, 0.4, 0.15); setTimeout(() => { AudioSystem.playGentle(AudioSystem.penta.C3 / 3, 0.3, 0.1); }, 100); } // Directional indicator toward attacker if (attackerPosition && worldState.player) { this.showDirectionalSkull(attackerPosition); } // Fade out after warning displayed setTimeout(() => { this.hidePremonition(); }, 800); }, // Show directional indicator toward attacker showDirectionalSkull(attackerPosition) { if (!worldState.player || !camera) return; // Calculate screen-space direction to attacker const playerPos = worldState.player.position; const dir = attackerPosition.clone().sub(playerPos).normalize(); // Create small directional skull indicator const indicator = document.createElement('div'); indicator.className = 'ghost-direction'; indicator.style.cssText = ` position: fixed; font-size: 48px; pointer-events: none; z-index: 10000; animation: pulse-ghost 0.3s ease-out; `; indicator.textContent = '💀'; // Position on edge of screen based on direction const angle = Math.atan2(dir.x, dir.z); const screenAngle = angle - camera.rotation.y; const edgeX = 50 + Math.sin(screenAngle) * 40; const edgeY = 50 - Math.cos(screenAngle) * 35; indicator.style.left = `${Math.max(5, Math.min(90, edgeX))}%`; indicator.style.top = `${Math.max(10, Math.min(85, edgeY))}%`; indicator.style.transform = 'translate(-50%, -50%)'; indicator.style.color = 'rgba(255, 0, 0, 0.8)'; indicator.style.textShadow = '0 0 20px red'; document.body.appendChild(indicator); // Remove after animation setTimeout(() => indicator.remove(), 600); }, // Hide the premonition // v7.82: Use cached DOM reference to avoid getElementById per hide hidePremonition() { this.isShowing = false; const container = getUICache().gameContainer; if (container) { container.style.boxShadow = ''; } if (this.ghostOverlay) { this.ghostOverlay.style.opacity = '0'; this.ghostOverlay.style.background = 'transparent'; const skull = this.ghostOverlay.querySelector('.ghost-skull'); const text = this.ghostOverlay.querySelector('.ghost-text'); if (skull) { skull.style.color = 'rgba(255, 0, 0, 0)'; skull.style.transform = 'scale(0.5)'; skull.style.filter = 'blur(2px)'; } if (text) { text.style.color = 'rgba(255, 50, 50, 0)'; } } }, // Check incoming attack and show warning if lethal checkAttack(incomingDamage, attackerPosition) { if (this.wouldBeLethal(incomingDamage)) { this.showDeathPremonition(attackerPosition); return true; // Attack is lethal } return false; // Attack is survivable } }; // Add CSS animation for ghost pulse const ghostStyle = document.createElement('style'); ghostStyle.textContent = ` @keyframes pulse-ghost { 0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; } 50% { transform: translate(-50%, -50%) scale(1.3); opacity: 1; } 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.8; } } `; document.head.appendChild(ghostStyle); // ============================================ // v6.35: BROWSER TAB CONSCIOUSNESS // 8-Agent Consensus Feature (4/8 strategies) // The game is aware when you leave and return // Creates eerie "the game knows you exist" moments // ============================================ const tabConsciousness = { isVisible: true, lastHiddenTime: null, originalTitle: document.title, tabSwitchCount: 0, totalAwayTime: 0, messages: [ "...waiting", "The void grows stronger", "Come back", "I can wait", "Are you there?", "LEVIATHAN remembers", "Time passes differently here", "The stars miss you" ], messageIndex: 0, titleInterval: null, init() { // v8.39: Use centralized visibility manager PageVisibilityManager.subscribe('audioContextManager', (isVisible) => { this.handleVisibilityChange(isVisible); }); window.addEventListener('blur', () => this.handleBlur()); window.addEventListener('focus', () => this.handleFocus()); }, handleVisibilityChange(isVisible) { if (!isVisible) { this.onHide(); } else { this.onShow(); } }, handleBlur() { if (!document.hidden) { this.onHide(); } }, handleFocus() { this.onShow(); }, onHide() { if (!this.isVisible) return; this.isVisible = false; this.lastHiddenTime = Date.now(); this.tabSwitchCount++; // Start cycling through messages in tab title // v7.43: Use TimerRegistry for centralized timer management (Cycle 22 Code Quality) TimerRegistry.setInterval('tab-consciousness-title', () => { this.messageIndex = (this.messageIndex + 1) % this.messages.length; document.title = this.messages[this.messageIndex]; }, 3000); // Immediate first message document.title = "...don't leave"; }, onShow() { if (this.isVisible) return; this.isVisible = true; // Stop title cycling // v7.43: Use TimerRegistry for centralized timer management (Cycle 22 Code Quality) TimerRegistry.clearInterval('tab-consciousness-title'); // Calculate time away const awayDuration = this.lastHiddenTime ? Date.now() - this.lastHiddenTime : 0; this.totalAwayTime += awayDuration; // Restore title document.title = this.originalTitle; // React to return based on how long player was away this.reactToReturn(awayDuration); }, reactToReturn(awayDuration) { const secondsAway = Math.floor(awayDuration / 1000); if (secondsAway < 5) return; // Ignore brief tab switches // Show notification based on duration let message; if (secondsAway > 300) { // 5+ minutes message = `The void watched over your absence. ${Math.floor(secondsAway / 60)} minutes have passed.`; // Subtle world change - spawn a watcher entity nearby this.spawnWatcher(); } else if (secondsAway > 60) { // 1-5 minutes message = `${secondsAway} seconds in your time... eons in the Omniverse.`; } else if (secondsAway > 10) { const returnMessages = [ "You returned. The Leviathan stirs.", "We knew you'd come back.", "Time moves strangely when you're away.", "The void remembers your absence." ]; message = returnMessages[Math.floor(Math.random() * returnMessages.length)]; } if (message && typeof showNotification === 'function') { setTimeout(() => showNotification(message, 'info'), 500); } // Track cumulative switches for later meta-awareness if (this.tabSwitchCount >= 10 && this.tabSwitchCount % 10 === 0) { setTimeout(() => { showNotification(`You have left ${this.tabSwitchCount} times. We notice patterns.`, 'info'); }, 2000); } }, spawnWatcher() { // Only in world mode if (mode !== 'world' || !worldState.player) return; // Create a brief, eerie visual - a dark shape at the edge of vision const watcherNotice = document.createElement('div'); watcherNotice.style.cssText = ` position: fixed; top: 50%; left: 10%; transform: translateY(-50%); width: 50px; height: 150px; background: radial-gradient(ellipse at center, rgba(20,0,40,0.8) 0%, transparent 70%); pointer-events: none; z-index: 100; opacity: 0; transition: opacity 2s ease-in-out; `; document.body.appendChild(watcherNotice); // Fade in briefly, then disappear setTimeout(() => watcherNotice.style.opacity = '0.6', 100); setTimeout(() => { watcherNotice.style.opacity = '0'; setTimeout(() => watcherNotice.remove(), 2000); }, 3000); } }; // Initialize tab consciousness setTimeout(() => tabConsciousness.init(), 1000); // ============================================ // v6.35: ENEMY PREMONITION GHOST // 8-Agent Consensus Feature (4/8 strategies) // See a ghost of the enemy's FUTURE attack // Shows where they WILL be, not where they ARE // ============================================ const enemyPremonition = { activeGhosts: [], maxGhosts: 5, // Create premonition ghost showing future attack position showPremonition(enemy, attackType) { if (mode !== 'world' || !enemy || !enemy.position) return; if (this.activeGhosts.length >= this.maxGhosts) return; // Calculate future position (where enemy will strike) // v7.91: Use GlobalVec3Pool instead of clone() const futurePos = GlobalVec3Pool.temp().copy(enemy.position); if (worldState.player) { // Enemy will move toward player const dir = GlobalVec3Pool.temp().subVectors(worldState.player.position, enemy.position).normalize(); const attackRange = enemy.userData?.attackRange || 2; futurePos.add(dir.multiplyScalar(attackRange * 0.8)); } // Create ghost mesh (translucent copy) const ghostMaterial = new THREE.MeshBasicMaterial({ color: 0xff0044, transparent: true, opacity: 0.3, wireframe: true }); let ghostMesh; if (enemy.geometry) { ghostMesh = new THREE.Mesh(enemy.geometry.clone(), ghostMaterial); } else { // Fallback simple shape ghostMesh = new THREE.Mesh( new THREE.SphereGeometry(0.5, 8, 8), ghostMaterial ); } ghostMesh.position.copy(futurePos); ghostMesh.position.y += 0.5; // Slight hover ghostMesh.scale.copy(enemy.scale); // Add pulsing animation data ghostMesh.userData = { startTime: performance.now(), duration: 800, baseOpacity: 0.3 }; scene.add(ghostMesh); this.activeGhosts.push(ghostMesh); // Auto-remove after duration setTimeout(() => this.removeGhost(ghostMesh), 800); // Play subtle warning tone if (AudioSystem && AudioSystem.penta) { AudioSystem.playGentle(AudioSystem.penta.E4 * 2, 0.15, 0.05); } }, // Update ghost animations update() { const now = performance.now(); for (const ghost of this.activeGhosts) { if (!ghost.userData) continue; const elapsed = now - ghost.userData.startTime; const progress = elapsed / ghost.userData.duration; // Pulse opacity const pulse = Math.sin(progress * Math.PI * 4) * 0.2; ghost.material.opacity = ghost.userData.baseOpacity + pulse; // Slight scale pulse const scalePulse = 1 + Math.sin(progress * Math.PI * 2) * 0.1; ghost.scale.setScalar(scalePulse); } }, removeGhost(ghost) { const idx = this.activeGhosts.indexOf(ghost); if (idx !== -1) { this.activeGhosts.splice(idx, 1); } if (ghost.parent) { ghost.parent.remove(ghost); } if (ghost.geometry) ghost.geometry.dispose(); if (ghost.material) ghost.material.dispose(); }, cleanup() { for (const ghost of [...this.activeGhosts]) { this.removeGhost(ghost); } } }; // ============================================ // v6.35: CHRONO-ECHO COMBAT // 8-Agent Consensus Feature (5/8 strategies) // Past actions replay as ghost clones // Your attacks echo 2 seconds later // ============================================ const chronoEcho = { enabled: true, echoDelay: 2000, // 2 second delay actionBuffer: [], // Records player actions maxBufferSize: 50, activeEchoes: [], // Record a player action recordAction(actionType, data) { if (!this.enabled || mode !== 'world') return; this.actionBuffer.push({ type: actionType, data: { ...data }, timestamp: performance.now(), playerPos: worldState.player ? worldState.player.position.clone() : null }); // Trim buffer if (this.actionBuffer.length > this.maxBufferSize) { this.actionBuffer.shift(); } // Schedule echo playback setTimeout(() => this.playbackEcho(actionType, data), this.echoDelay); }, // Playback an echoed action playbackEcho(actionType, data) { if (mode !== 'world' || !worldState.player) return; switch (actionType) { case 'attack': this.echoAttack(data); break; case 'ability': this.echoAbility(data); break; } }, // Echo an attack - creates ghost damage echoAttack(data) { if (!data.targetPos) return; // Visual: ghost slash effect this.spawnEchoVisual(data.targetPos); // v7.79: Find enemies near echo position - distanceToSquared optimization const echoRangeSq = 9; // 3 * 3 const echoDamage = Math.floor((data.damage || 5) * 0.5); // 50% echo damage for (const mob of worldState.mobs) { if (!mob.userData || mob.userData.hp <= 0) continue; const distSq = mob.position.distanceToSquared(data.targetPos); if (distSq < echoRangeSq) { // Apply echo damage mob.userData.hp -= echoDamage; spawnFloater(mob.position, `ECHO -${echoDamage}`, '#8888ff'); // Update health bar if (mob.userData.hpBar) { const hpPercent = mob.userData.hp / mob.userData.maxHp; mob.userData.hpBar.scale.x = Math.max(0.01, hpPercent); } // Play echo sound if (AudioSystem && AudioSystem.penta) { AudioSystem.playGentle(AudioSystem.penta.G4, 0.15, 0.08); } } } }, // Echo an ability echoAbility(data) { // Visual only for abilities - too complex to fully replicate if (data.position) { this.spawnEchoVisual(data.position, true); } }, // Spawn visual echo effect spawnEchoVisual(position, isAbility = false) { if (!position) return; // Create ghostly ring effect const ringGeo = new THREE.RingGeometry(0.5, 1.5, 16); const ringMat = new THREE.MeshBasicMaterial({ color: isAbility ? 0x44ffff : 0x8888ff, transparent: true, opacity: 0.6, side: THREE.DoubleSide }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.position.copy(position); ring.position.y += 0.1; ring.rotation.x = -Math.PI / 2; ring.userData = { startTime: performance.now(), duration: 500 }; scene.add(ring); this.activeEchoes.push(ring); // Particles if (particles) { particles.emit(position, 8, isAbility ? 0x44ffff : 0x8888ff, { spread: 2, lifetime: 400, size: 0.1 }); } // Auto cleanup setTimeout(() => { const idx = this.activeEchoes.indexOf(ring); if (idx !== -1) this.activeEchoes.splice(idx, 1); if (ring.parent) ring.parent.remove(ring); ringGeo.dispose(); ringMat.dispose(); }, 500); }, // Update echo visuals update() { const now = performance.now(); for (const echo of this.activeEchoes) { if (!echo.userData) continue; const progress = (now - echo.userData.startTime) / echo.userData.duration; // Expand and fade echo.scale.setScalar(1 + progress * 2); echo.material.opacity = 0.6 * (1 - progress); } } }; // ============================================ // v6.35: COMBO CRESCENDO ORCHESTRA // 8-Agent Consensus Feature (4/8 strategies) // Combat performance composes music in real-time // Higher combos add more instrument layers // ============================================ const comboCrescendo = { layers: { bass: null, harmony: null, melody: null, percussion: null }, currentCombo: 0, lastNoteTime: 0, noteInterval: 250, // ms between notes scale: null, // Will use AudioSystem.penta // Initialize (called when entering combat) startCombat() { this.currentCombo = 0; this.stopAllLayers(); }, // Update based on combo count updateCombo(comboCount) { if (!AudioSystem || !AudioSystem.penta) return; this.scale = AudioSystem.penta; this.currentCombo = comboCount; const now = performance.now(); if (now - this.lastNoteTime < this.noteInterval) return; this.lastNoteTime = now; // Layer 1: Base hits (always) this.playBaseHit(comboCount); // Layer 2: Harmony (5+ combo) if (comboCount >= 5) { this.playHarmony(comboCount); // v8.0: Pet celebrates combo milestones! (8-Agent Consensus Cycle 5) if (comboCount === 5 && typeof triggerPetReaction === 'function') { triggerPetReaction('combo5'); } } // Layer 3: Melody (10+ combo) if (comboCount >= 10) { this.playMelody(comboCount); // v8.0: Pet celebrates 10x combo! (8-Agent Consensus Cycle 5) if (comboCount === 10 && typeof triggerPetReaction === 'function') { triggerPetReaction('combo10'); } } // Layer 4: Percussion accent (15+ combo) if (comboCount >= 15) { this.playPercussion(comboCount); } // Layer 5: Full crescendo (25+ combo) if (comboCount >= 25 && comboCount % 5 === 0) { this.playFullCrescendo(); } }, playBaseHit(combo) { // Ascending notes based on combo const noteIndex = combo % 5; const notes = [this.scale.C3, this.scale.D3, this.scale.E3, this.scale.G3, this.scale.A3]; AudioSystem.playGentle(notes[noteIndex], 0.12, 0.08); }, playHarmony(combo) { // Add fifth harmony const noteIndex = combo % 5; const notes = [this.scale.G3, this.scale.A3, this.scale.C4, this.scale.D4, this.scale.E4]; setTimeout(() => { AudioSystem.playGentle(notes[noteIndex], 0.08, 0.06); }, 50); }, playMelody(combo) { // Higher octave melody const noteIndex = combo % 5; const notes = [this.scale.C4, this.scale.E4, this.scale.G4, this.scale.A4, this.scale.C4 * 2]; setTimeout(() => { AudioSystem.playGentle(notes[noteIndex], 0.1, 0.1); }, 100); }, playPercussion(combo) { // Rhythmic accent using noise-like tones const freq = 80 + (combo % 4) * 20; AudioSystem.playGentle(freq, 0.05, 0.02); }, playFullCrescendo() { // Dramatic chord swell const chord = [this.scale.C3, this.scale.E3, this.scale.G3, this.scale.C4]; chord.forEach((note, i) => { setTimeout(() => { AudioSystem.playGentle(note, 0.15, 0.15); }, i * 30); }); }, // Called when combo breaks - dramatic resolution comboBreak(finalCombo) { if (!AudioSystem || !AudioSystem.penta || finalCombo < 5) return; // Descending resolution const resolution = [ this.scale.G4, this.scale.E4, this.scale.D4, this.scale.C4, this.scale.C3 ]; resolution.forEach((note, i) => { setTimeout(() => { const volume = 0.15 - i * 0.02; AudioSystem.playGentle(note, 0.2, Math.max(0.05, volume)); }, i * 100); }); }, stopAllLayers() { // Cleanup any sustained tones this.currentCombo = 0; } }; // ============================================ // v6.36: IMPACT SCREEN SHAKE SYSTEM // Camera shake proportional to damage dealt/received // Consensus feature from Round 3 strategy analysis // ============================================ const impactShake = { enabled: true, intensity: 0, decay: 0.92, maxIntensity: 15, shakeOffset: { x: 0, y: 0 }, // Trigger shake based on damage triggerDamageDealt(damage) { if (!this.enabled) return; // Scale shake by damage - big hits = big shake const intensity = Math.min(damage * 0.8, this.maxIntensity * 0.6); this.intensity = Math.max(this.intensity, intensity); }, triggerDamageReceived(damage) { if (!this.enabled) return; // Taking damage shakes more than dealing it const intensity = Math.min(damage * 1.2, this.maxIntensity); this.intensity = Math.max(this.intensity, intensity); }, triggerKill() { if (!this.enabled) return; // Satisfying kill shake this.intensity = Math.max(this.intensity, 8); }, triggerBossHit() { if (!this.enabled) return; // Boss hits feel massive this.intensity = Math.max(this.intensity, 12); }, update() { if (this.intensity > 0.1) { // Random directional shake this.shakeOffset.x = (Math.random() - 0.5) * this.intensity * 2; this.shakeOffset.y = (Math.random() - 0.5) * this.intensity * 2; this.intensity *= this.decay; // Apply to camera or container const container = document.getElementById('container'); if (container) { container.style.transform = `translate(${this.shakeOffset.x}px, ${this.shakeOffset.y}px)`; } } else { this.intensity = 0; this.shakeOffset.x = 0; this.shakeOffset.y = 0; const container = document.getElementById('container'); if (container) { container.style.transform = ''; } } } }; // ============================================ // v6.36: PERSONAL RECORDS DASHBOARD // Track all-time stats, best combos, fastest kills // Consensus feature from Round 3 strategy analysis // ============================================ const personalRecords = { storageKey: 'leviathan-records-v1', records: null, // v6.84: Added error handling for corrupted records data // v8.0: Now using SafeJSON utility (8-Strategy Consensus Cycle 3) init() { const defaultRecords = { totalKills: 0, totalDamageDealt: 0, totalDamageTaken: 0, highestCombo: 0, fastestBossKill: Infinity, longestSession: 0, totalPlayTime: 0, planetsConquered: 0, bestSingleHit: 0, totalDeaths: 0, dailyKills: 0, lastPlayDate: null, currentStreak: 0, bestStreak: 0, sessionsPlayed: 0 }; this.records = SafeJSON.fromLocalStorage(this.storageKey, defaultRecords); this.sessionStart = Date.now(); // Check daily reset const today = new Date().toDateString(); if (this.records.lastPlayDate !== today) { if (this.records.lastPlayDate) { const lastDate = new Date(this.records.lastPlayDate); const todayDate = new Date(today); const daysDiff = Math.floor((todayDate - lastDate) / (1000 * 60 * 60 * 24)); if (daysDiff === 1) { this.records.currentStreak++; if (this.records.currentStreak > this.records.bestStreak) { this.records.bestStreak = this.records.currentStreak; this.showStreakAchievement(); } } else if (daysDiff > 1) { this.records.currentStreak = 1; } } else { this.records.currentStreak = 1; } this.records.dailyKills = 0; this.records.lastPlayDate = today; } this.records.sessionsPlayed++; this.save(); }, // v8.29: Throttled save to prevent excessive localStorage writes _saveTimeout: null, _lastSave: 0, _saveThrottleMs: 2000, // Only save at most once per 2 seconds save() { localStorage.setItem(this.storageKey, JSON.stringify(this.records)); this._lastSave = performance.now(); }, // v8.29: Throttled save - queues save for later if called too frequently throttledSave() { const now = performance.now(); if (now - this._lastSave >= this._saveThrottleMs) { this.save(); } else { // Queue a save if not already queued if (!this._saveTimeout) { this._saveTimeout = setTimeout(() => { this.save(); this._saveTimeout = null; }, this._saveThrottleMs); } } }, recordKill() { this.records.totalKills++; this.records.dailyKills++; this.throttledSave(); // v8.29: Use throttled save }, recordDamageDealt(amount) { this.records.totalDamageDealt += amount; if (amount > this.records.bestSingleHit) { this.records.bestSingleHit = amount; this.showNewRecord('BEST HIT', amount); } this.throttledSave(); // v8.29: Use throttled save }, recordCombo(combo) { if (combo > this.records.highestCombo) { this.records.highestCombo = combo; this.showNewRecord('HIGHEST COMBO', combo); } this.throttledSave(); // v8.29: Use throttled save }, recordDeath() { this.records.totalDeaths++; this.throttledSave(); // v8.29: Use throttled save }, recordPlanetConquered() { this.records.planetsConquered++; this.save(); // Immediate save for milestone }, updateSessionTime() { const sessionLength = Math.floor((Date.now() - this.sessionStart) / 1000); this.records.totalPlayTime += 1; // Add 1 second if (sessionLength > this.records.longestSession) { this.records.longestSession = sessionLength; } this.throttledSave(); // v8.29: Use throttled save (called frequently) }, showNewRecord(type, value) { // v8.0: Use enhanced Personal Best Celebration system (8-Agent Consensus Cycle 4) if (typeof triggerPersonalBestCelebration === 'function') { // Get previous best for improvement calculation let previousBest = 0; switch (type) { case 'BEST HIT': previousBest = this.records.bestSingleHit - value; // Value is already updated break; case 'HIGHEST COMBO': previousBest = this.records.highestCombo - value; break; } triggerPersonalBestCelebration(type, value, Math.max(0, previousBest)); return; } // Fallback to original display const recordDiv = document.createElement('div'); recordDiv.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: linear-gradient(135deg, rgba(255,215,0,0.9), rgba(255,165,0,0.9)); color: #000; padding: 20px 40px; border-radius: 10px; font-size: 24px; font-weight: bold; z-index: 10000; text-align: center; animation: recordPulse 0.5s ease-out; box-shadow: 0 0 30px rgba(255,215,0,0.8); `; recordDiv.innerHTML = `
🏆 NEW RECORD!
${type}
${value}
`; document.body.appendChild(recordDiv); setTimeout(() => { recordDiv.style.opacity = '0'; recordDiv.style.transition = 'opacity 0.5s'; setTimeout(() => recordDiv.remove(), 500); }, 2000); }, showStreakAchievement() { // v8.0: Use enhanced Personal Best Celebration for streaks (8-Agent Consensus Cycle 4) if (typeof triggerPersonalBestCelebration === 'function') { triggerPersonalBestCelebration('Day Streak', this.records.currentStreak, this.records.bestStreak - 1); return; } // Fallback const streakDiv = document.createElement('div'); streakDiv.style.cssText = ` position: fixed; top: 30%; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, rgba(255,100,0,0.9), rgba(255,50,0,0.9)); color: #fff; padding: 15px 30px; border-radius: 10px; font-size: 20px; font-weight: bold; z-index: 10000; text-align: center; animation: recordPulse 0.5s ease-out; `; streakDiv.innerHTML = `🔥 ${this.records.currentStreak} DAY STREAK! 🔥`; document.body.appendChild(streakDiv); setTimeout(() => { streakDiv.style.opacity = '0'; streakDiv.style.transition = 'opacity 0.5s'; setTimeout(() => streakDiv.remove(), 500); }, 2500); }, getStatsDisplay() { const hours = Math.floor(this.records.totalPlayTime / 3600); const mins = Math.floor((this.records.totalPlayTime % 3600) / 60); return { 'Total Kills': this.records.totalKills.toLocaleString(), 'Best Combo': this.records.highestCombo, 'Best Hit': this.records.bestSingleHit, 'Planets': this.records.planetsConquered, 'Play Time': `${hours}h ${mins}m`, 'Day Streak': `🔥 ${this.records.currentStreak}`, 'Best Streak': this.records.bestStreak, 'Sessions': this.records.sessionsPlayed }; } }; // ============================================ // v8.0: PERSONAL BEST STREAK CELEBRATION - 8-Agent Consensus (Cycle 4) // Epic celebrations when players break personal records! // ============================================ const PERSONAL_BEST_CONFIG = { // Milestone thresholds for escalating celebrations COMBO_MILESTONES: [10, 25, 50, 100, 200], KILL_MILESTONES: [100, 500, 1000, 5000, 10000], DAMAGE_MILESTONES: [1000, 5000, 10000, 50000, 100000], STREAK_MILESTONES: [3, 7, 14, 30, 100], // Audio frequencies for celebration fanfare FANFARE_NOTES: { bronze: [523.25, 659.25, 783.99], // C5-E5-G5 (simple) silver: [523.25, 659.25, 783.99, 1046.50], // C5-E5-G5-C6 gold: [392.00, 493.88, 587.33, 783.99, 987.77], // G4-B4-D5-G5-B5 platinum: [392.00, 493.88, 587.33, 783.99, 987.77, 1174.66, 1567.98] // Full arpeggio } }; function getPersonalBestTier(milestoneIndex) { if (milestoneIndex >= 4) return 'platinum'; if (milestoneIndex >= 3) return 'gold'; if (milestoneIndex >= 2) return 'silver'; return 'bronze'; } function playPersonalBestFanfare(tier = 'bronze') { // v7.28: Use shared AudioContext const audioCtx = getSharedAudioContext(); if (!audioCtx) return; try { const notes = PERSONAL_BEST_CONFIG.FANFARE_NOTES[tier] || PERSONAL_BEST_CONFIG.FANFARE_NOTES.bronze; const masterGain = audioCtx.createGain(); masterGain.gain.value = 0.25; masterGain.connect(audioCtx.destination); notes.forEach((freq, i) => { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; // Staggered timing for arpeggio effect const startTime = audioCtx.currentTime + (i * 0.12); const duration = 0.4 + (i * 0.1); gain.gain.setValueAtTime(0, startTime); gain.gain.linearRampToValueAtTime(0.6, startTime + 0.05); gain.gain.linearRampToValueAtTime(0.3, startTime + duration * 0.6); gain.gain.linearRampToValueAtTime(0, startTime + duration); osc.connect(gain); gain.connect(masterGain); osc.start(startTime); osc.stop(startTime + duration); }); // Add shimmer/sparkle effect for gold and platinum if (tier === 'gold' || tier === 'platinum') { for (let i = 0; i < 5; i++) { const shimmer = audioCtx.createOscillator(); const shimmerGain = audioCtx.createGain(); shimmer.type = 'sine'; shimmer.frequency.value = 2000 + Math.random() * 2000; shimmerGain.gain.setValueAtTime(0, audioCtx.currentTime + 0.5 + i * 0.1); shimmerGain.gain.linearRampToValueAtTime(0.1, audioCtx.currentTime + 0.55 + i * 0.1); shimmerGain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.8 + i * 0.1); shimmer.connect(shimmerGain); shimmerGain.connect(masterGain); shimmer.start(audioCtx.currentTime + 0.5 + i * 0.1); shimmer.stop(audioCtx.currentTime + 1 + i * 0.1); } } } catch (e) { console.log('Personal best fanfare audio error:', e); } } function triggerPersonalBestCelebration(recordType, newValue, previousBest) { const tier = determineRecordTier(recordType, newValue); playPersonalBestFanfare(tier); // Create epic visual celebration const celebrationDiv = document.createElement('div'); const tierColors = { bronze: 'linear-gradient(135deg, #cd7f32, #8b4513)', silver: 'linear-gradient(135deg, #c0c0c0, #808080)', gold: 'linear-gradient(135deg, #ffd700, #ff8c00)', platinum: 'linear-gradient(135deg, #e5e4e2, #a0d2db, #ffd700)' }; const tierGlow = { bronze: '0 0 30px rgba(205,127,50,0.8)', silver: '0 0 40px rgba(192,192,192,0.9)', gold: '0 0 50px rgba(255,215,0,1)', platinum: '0 0 60px rgba(255,255,255,1), 0 0 80px rgba(255,215,0,0.5)' }; const tierEmoji = { bronze: '🥉', silver: '🥈', gold: '🥇', platinum: '💎' }; celebrationDiv.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0); background: ${tierColors[tier]}; color: ${tier === 'bronze' ? '#fff' : '#000'}; padding: 30px 50px; border-radius: 15px; font-size: 28px; font-weight: bold; z-index: 10001; text-align: center; box-shadow: ${tierGlow[tier]}; animation: personalBestZoom 0.5s ease-out forwards; `; // Add keyframe animation if not exists if (!document.getElementById('personal-best-styles')) { const style = document.createElement('style'); style.id = 'personal-best-styles'; style.textContent = ` @keyframes personalBestZoom { 0% { transform: translate(-50%, -50%) scale(0) rotate(-10deg); opacity: 0; } 50% { transform: translate(-50%, -50%) scale(1.2) rotate(5deg); } 100% { transform: translate(-50%, -50%) scale(1) rotate(0deg); opacity: 1; } } @keyframes personalBestPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } @keyframes particleBurst { 0% { transform: translate(0, 0) scale(1); opacity: 1; } 100% { transform: translate(var(--tx), var(--ty)) scale(0); opacity: 0; } } `; document.head.appendChild(style); } const improvement = previousBest > 0 ? ((newValue - previousBest) / previousBest * 100).toFixed(1) : 'NEW'; celebrationDiv.innerHTML = `
${tierEmoji[tier]}
PERSONAL BEST!
${recordType.toUpperCase()}
${newValue.toLocaleString()}
${typeof improvement === 'string' ? improvement : `+${improvement}% improvement!`}
`; document.body.appendChild(celebrationDiv); // Spawn particle burst for higher tiers if (tier !== 'bronze') { spawnPersonalBestParticles(tier); } // Screen shake for gold and platinum if (tier === 'gold' || tier === 'platinum') { screenShake(tier === 'platinum' ? 0.4 : 0.25); } // Remove after display setTimeout(() => { celebrationDiv.style.animation = 'personalBestZoom 0.3s ease-in reverse forwards'; setTimeout(() => celebrationDiv.remove(), 300); }, 3500); } function spawnPersonalBestParticles(tier) { const particleCount = tier === 'platinum' ? 40 : tier === 'gold' ? 25 : 15; const colors = { silver: ['#c0c0c0', '#a0a0a0', '#e0e0e0'], gold: ['#ffd700', '#ff8c00', '#ffec8b'], platinum: ['#e5e4e2', '#a0d2db', '#ffd700', '#ffffff'] }; for (let i = 0; i < particleCount; i++) { const particle = document.createElement('div'); const angle = (i / particleCount) * Math.PI * 2; const distance = 100 + Math.random() * 150; const tx = Math.cos(angle) * distance; const ty = Math.sin(angle) * distance; const color = colors[tier][Math.floor(Math.random() * colors[tier].length)]; particle.style.cssText = ` position: fixed; top: 50%; left: 50%; width: ${8 + Math.random() * 12}px; height: ${8 + Math.random() * 12}px; background: ${color}; border-radius: 50%; z-index: 10000; pointer-events: none; --tx: ${tx}px; --ty: ${ty}px; animation: particleBurst ${0.6 + Math.random() * 0.4}s ease-out forwards; animation-delay: ${Math.random() * 0.2}s; `; document.body.appendChild(particle); setTimeout(() => particle.remove(), 1200); } } function determineRecordTier(recordType, value) { let milestones; switch (recordType.toLowerCase()) { case 'combo': case 'highest combo': milestones = PERSONAL_BEST_CONFIG.COMBO_MILESTONES; break; case 'kills': case 'total kills': milestones = PERSONAL_BEST_CONFIG.KILL_MILESTONES; break; case 'damage': case 'best hit': milestones = PERSONAL_BEST_CONFIG.DAMAGE_MILESTONES; break; case 'streak': case 'day streak': milestones = PERSONAL_BEST_CONFIG.STREAK_MILESTONES; break; default: return 'bronze'; } let milestoneIndex = 0; for (let i = 0; i < milestones.length; i++) { if (value >= milestones[i]) milestoneIndex = i; } return getPersonalBestTier(milestoneIndex); } // ============================================ // v6.36: DAILY CHALLENGE SYSTEM // Rotating challenges with streak bonuses // Consensus feature from Round 3 strategy analysis // ============================================ const dailyChallenges = { storageKey: 'leviathan-daily-v1', challenges: [], challengeTemplates: [ { id: 'kills', name: 'Slayer', desc: 'Defeat {target} enemies', targets: [25, 50, 100], icon: '⚔️' }, { id: 'combo', name: 'Combo Master', desc: 'Reach a {target}x combo', targets: [15, 25, 50], icon: '🔥' }, { id: 'damage', name: 'Heavy Hitter', desc: 'Deal {target} total damage', targets: [500, 1000, 2500], icon: '💥' }, { id: 'nodeath', name: 'Untouchable', desc: 'Kill {target} enemies without dying', targets: [10, 20, 30], icon: '🛡️' }, { id: 'ability', name: 'Ability Expert', desc: 'Use abilities {target} times', targets: [20, 40, 60], icon: '✨' }, { id: 'dash', name: 'Speed Demon', desc: 'Dash {target} times', targets: [30, 50, 100], icon: '💨' } ], // v8.0: Using SafeJSON for daily challenges (8-Strategy Consensus Cycle 8) init() { const data = SafeJSON.fromLocalStorage(this.storageKey, { date: null, challenges: [], progress: {} }); const today = new Date().toDateString(); if (data.date !== today) { // Generate new daily challenges this.generateDailyChallenges(today); } else { this.challenges = data.challenges; this.progress = data.progress; } }, generateDailyChallenges(date) { // Seed random based on date for consistent daily challenges const seed = date.split('').reduce((a, c) => a + c.charCodeAt(0), 0); const seededRandom = (i) => { const x = Math.sin(seed + i) * 10000; return x - Math.floor(x); }; // Pick 3 unique challenges const shuffled = [...this.challengeTemplates].sort(() => seededRandom(Math.random()) - 0.5); this.challenges = shuffled.slice(0, 3).map((template, i) => { const difficulty = Math.floor(seededRandom(i) * 3); return { ...template, target: template.targets[difficulty], difficulty: ['Easy', 'Medium', 'Hard'][difficulty], completed: false }; }); this.progress = { kills: 0, combo: 0, damage: 0, killsWithoutDeath: 0, abilityUses: 0, dashes: 0 }; this.save(date); this.showDailyChallenges(); }, save(date) { localStorage.setItem(this.storageKey, JSON.stringify({ date: date || new Date().toDateString(), challenges: this.challenges, progress: this.progress })); }, updateProgress(type, value) { if (!this.progress) return; switch(type) { case 'kill': this.progress.kills++; this.progress.killsWithoutDeath++; break; case 'combo': this.progress.combo = Math.max(this.progress.combo, value); break; case 'damage': this.progress.damage += value; break; case 'death': this.progress.killsWithoutDeath = 0; break; case 'ability': this.progress.abilityUses++; break; case 'dash': this.progress.dashes++; break; } this.checkChallengeCompletion(); this.save(); }, checkChallengeCompletion() { this.challenges.forEach(challenge => { if (challenge.completed) return; let current = 0; switch(challenge.id) { case 'kills': current = this.progress.kills; break; case 'combo': current = this.progress.combo; break; case 'damage': current = this.progress.damage; break; case 'nodeath': current = this.progress.killsWithoutDeath; break; case 'ability': current = this.progress.abilityUses; break; case 'dash': current = this.progress.dashes; break; } if (current >= challenge.target) { challenge.completed = true; this.showChallengeComplete(challenge); } }); }, showChallengeComplete(challenge) { const div = document.createElement('div'); div.style.cssText = ` position: fixed; top: 20%; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, rgba(0,255,100,0.95), rgba(0,200,80,0.95)); color: #000; padding: 20px 40px; border-radius: 15px; font-size: 22px; font-weight: bold; z-index: 10000; text-align: center; animation: recordPulse 0.5s ease-out; box-shadow: 0 0 40px rgba(0,255,100,0.6); `; div.innerHTML = `
${challenge.icon}
CHALLENGE COMPLETE!
${challenge.name}
`; document.body.appendChild(div); // Play celebration sound AudioSystem.playGentle(880, 0.1, 0.3); setTimeout(() => AudioSystem.playGentle(1100, 0.1, 0.3), 100); setTimeout(() => AudioSystem.playGentle(1320, 0.15, 0.3), 200); setTimeout(() => { div.style.opacity = '0'; div.style.transition = 'opacity 0.5s'; setTimeout(() => div.remove(), 500); }, 3000); }, showDailyChallenges() { const div = document.createElement('div'); div.style.cssText = ` position: fixed; top: 15%; left: 50%; transform: translateX(-50%); background: rgba(0,20,40,0.95); border: 2px solid #0ff; color: #fff; padding: 20px 30px; border-radius: 15px; font-size: 16px; z-index: 10000; text-align: center; box-shadow: 0 0 30px rgba(0,255,255,0.4); `; div.innerHTML = `
📋 TODAY'S CHALLENGES
${this.challenges.map(c => `
${c.icon} ${c.name} [${c.difficulty}]
${c.desc.replace('{target}', c.target)}
`).join('')} `; document.body.appendChild(div); setTimeout(() => { div.style.opacity = '0'; div.style.transition = 'opacity 0.5s'; setTimeout(() => div.remove(), 500); }, 5000); }, getChallengeProgress() { return this.challenges.map(c => { let current = 0; switch(c.id) { case 'kills': current = this.progress?.kills || 0; break; case 'combo': current = this.progress?.combo || 0; break; case 'damage': current = this.progress?.damage || 0; break; case 'nodeath': current = this.progress?.killsWithoutDeath || 0; break; case 'ability': current = this.progress?.abilityUses || 0; break; case 'dash': current = this.progress?.dashes || 0; break; } return { ...c, current, percent: Math.min(100, (current / c.target) * 100) }; }); } }; // ============================================ // v7.0: LOGIN STREAK CALENDAR SYSTEM // 7-day calendar with escalating rewards // Consensus feature from 8-Strategy Analysis (9/10 impact) // ============================================ const LoginStreakCalendar = { STORAGE_KEY: 'leviathan-login-streak-v1', data: { lastLoginDate: null, currentStreak: 0, longestStreak: 0, totalLogins: 0, monthlyProgress: 0, claimedRewards: [], lastStreakBreakDate: null }, // Escalating rewards for each day of the week DAILY_REWARDS: [ { day: 1, xp: 100, essence: 5, icon: '🌟', name: 'Day 1' }, { day: 2, xp: 150, essence: 10, icon: '✨', name: 'Day 2' }, { day: 3, xp: 200, essence: 15, icon: '💫', name: 'Day 3', bonus: 'Focus Boost' }, { day: 4, xp: 300, essence: 25, icon: '🔮', name: 'Day 4' }, { day: 5, xp: 400, essence: 35, icon: '💎', name: 'Day 5', bonus: 'Rare Item' }, { day: 6, xp: 500, essence: 50, icon: '👑', name: 'Day 6' }, { day: 7, xp: 1000, essence: 100, icon: '🏆', name: 'Day 7', bonus: 'Legendary Reward', legendary: true } ], init() { // v8.0: Using SafeJSON for Login Streak data (8-Strategy Consensus Cycle 5) const saved = SafeJSON.fromLocalStorage(this.STORAGE_KEY, null); if (saved) { this.data = { ...this.data, ...saved }; } this.checkLogin(); }, checkLogin() { const today = new Date().toDateString(); const yesterday = new Date(Date.now() - 86400000).toDateString(); if (this.data.lastLoginDate === today) { // Already logged in today console.log('[LOGIN STREAK] Already logged in today. Streak:', this.data.currentStreak); return; } // Check if streak continues or breaks if (this.data.lastLoginDate === yesterday) { // Streak continues! this.data.currentStreak++; console.log('[LOGIN STREAK] Streak continued!', this.data.currentStreak); } else if (this.data.lastLoginDate) { // Streak broken this.data.lastStreakBreakDate = this.data.lastLoginDate; this.data.currentStreak = 1; this.data.claimedRewards = []; console.log('[LOGIN STREAK] Streak broken. Starting fresh.'); } else { // First ever login this.data.currentStreak = 1; } this.data.lastLoginDate = today; this.data.totalLogins++; this.data.monthlyProgress++; if (this.data.currentStreak > this.data.longestStreak) { this.data.longestStreak = this.data.currentStreak; } this.save(); // Show calendar after a brief delay setTimeout(() => this.showCalendarModal(), 2000); }, save() { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.data)); }, claimReward(day) { if (this.data.claimedRewards.includes(day)) { showNotification('Already claimed!', 'warning'); return; } if (day > this.data.currentStreak) { showNotification('Keep your streak to unlock this reward!', 'warning'); return; } const reward = this.DAILY_REWARDS[day - 1]; this.data.claimedRewards.push(day); this.save(); // Grant rewards if (typeof gameData !== 'undefined') { gameData.totalXP = (gameData.totalXP || 0) + reward.xp; gameData.focusEssence = (gameData.focusEssence || 0) + reward.essence; if (typeof saveGameData === 'function') saveGameData(); } // Celebration this.showRewardClaimed(reward); // Update calendar UI const dayEl = document.querySelector(`[data-streak-day="${day}"]`); if (dayEl) { dayEl.classList.add('claimed'); dayEl.querySelector('.claim-btn')?.remove(); } }, showRewardClaimed(reward) { const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10001; animation: fadeIn 0.3s ease-out; `; overlay.innerHTML = `
${reward.icon}

REWARD CLAIMED!

+${reward.xp} XP
+${reward.essence} Focus Essence
${reward.bonus ? `
✧ ${reward.bonus} ✧
` : ''}
`; document.body.appendChild(overlay); // Celebration audio AudioSystem.playGentle(523.25, 0.15, 0.3); setTimeout(() => AudioSystem.playGentle(659.25, 0.15, 0.3), 100); setTimeout(() => AudioSystem.playGentle(783.99, 0.15, 0.3), 200); if (reward.legendary) { setTimeout(() => AudioSystem.playGentle(1046.50, 0.25, 0.4), 300); } setTimeout(() => overlay.remove(), 5000); }, showCalendarModal() { const existingModal = document.getElementById('login-streak-modal'); if (existingModal) existingModal.remove(); const modal = document.createElement('div'); modal.id = 'login-streak-modal'; // v7.78: Added ARIA attributes for accessibility modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-labelledby', 'login-streak-title'); modal.innerHTML = `

🔥 LOGIN STREAK

${this.data.currentStreak}
Day Streak
Best: ${this.data.longestStreak} days | Total logins: ${this.data.totalLogins}
${this.DAILY_REWARDS.map((r, i) => { const day = i + 1; const unlocked = day <= this.data.currentStreak; const claimed = this.data.claimedRewards.includes(day); const isToday = day === this.data.currentStreak; return `
${r.icon}
${r.name}
+${r.xp} XP
+${r.essence} ✧
${claimed ? '
✓ Claimed
' : unlocked ? `` : '
🔒
'}
`; }).join('')}
${this.data.currentStreak >= 7 ? `
🏆 WEEKLY STREAK COMPLETE! 🏆
You're a dedication legend! Keep it going!
` : `
Come back tomorrow to continue your streak!
`}
`; document.body.appendChild(modal); }, // Get current streak for HUD display getStreak() { return this.data.currentStreak; } }; // ============================================ // v7.1: WORLD CHAT SYSTEM (Round 2 Consensus - 9/10 Impact) // Real-time player-to-player chat for multiplayer sessions // Uses existing P2P delta system for message broadcast // ============================================ const WorldChat = { isOpen: false, isMinimized: false, messages: [], unreadCount: 0, maxMessages: 50, playerName: null, init() { // v8.27: Sanitize player name from gameData const rawName = gameData?.playerName || `Explorer_${Math.random().toString(36).substring(2, 6)}`; this.playerName = sanitizeUserInput(rawName, { maxLength: 30, defaultValue: 'Explorer' }); // v9.2: Changed keyboard shortcut from T to Enter to avoid conflict with Heal ability document.addEventListener('keydown', (e) => { if (e.key === 'Enter') { // Don't trigger if typing in another input (except world chat itself) const activeEl = document.activeElement; const isWorldChatInput = activeEl && activeEl.id === 'world-chat-input'; if (activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA') && !isWorldChatInput) { return; } // If chat is closed, open it and focus if (!this.isOpen) { e.preventDefault(); this.toggle(); setTimeout(() => { const input = document.getElementById('world-chat-input'); if (input) input.focus(); }, 100); } // If already in chat input, Enter sends the message (handled by input's onkeypress) } // Escape to close chat if (e.key === 'Escape' && this.isOpen) { e.preventDefault(); this.toggle(); // Blur the input to return focus to game const input = document.getElementById('world-chat-input'); if (input) input.blur(); } }); console.log('[WORLD CHAT] Initialized'); }, // Show toggle button when multiplayer is active showToggle() { const toggle = document.getElementById('world-chat-toggle'); if (toggle) toggle.classList.add('active'); }, hideToggle() { const toggle = document.getElementById('world-chat-toggle'); if (toggle) toggle.classList.remove('active'); this.close(); }, toggle() { if (this.isOpen) { this.close(); } else { this.open(); } }, open() { this.isOpen = true; this.unreadCount = 0; this.updateUnreadBadge(); const container = document.getElementById('world-chat-container'); const toggle = document.getElementById('world-chat-toggle'); if (container) container.classList.add('active'); if (toggle) toggle.classList.remove('has-unread'); }, close() { this.isOpen = false; const container = document.getElementById('world-chat-container'); if (container) container.classList.remove('active'); }, toggleMinimize() { this.isMinimized = !this.isMinimized; const container = document.getElementById('world-chat-container'); if (container) { if (this.isMinimized) { container.classList.add('minimized'); } else { container.classList.remove('minimized'); } } }, send() { const input = document.getElementById('world-chat-input'); if (!input) return; const text = input.value.trim(); if (!text) return; // Add to local messages this.addMessage(this.playerName, text, true); // Broadcast to other players via P2P this.broadcast(text); // Clear input input.value = ''; }, broadcast(text) { // Use existing P2P system to send chat message if (typeof broadcastDelta === 'function') { broadcastDelta({ type: 'chatMessage', sender: this.playerName, text: text, timestamp: Date.now() }); } // Also broadcast via PublicWorldManager if active if (window.PublicWorldManager && PublicWorldManager.isHost && PublicWorldManager.participants) { PublicWorldManager.participants.forEach(conn => { if (conn && conn.open) { conn.send({ type: 'WORLD_CHAT', sender: this.playerName, text: text, timestamp: Date.now() }); } }); } else if (window.PublicWorldManager && PublicWorldManager.hostConnection) { PublicWorldManager.hostConnection.send({ type: 'WORLD_CHAT', sender: this.playerName, text: text, timestamp: Date.now() }); } }, receiveMessage(sender, text, timestamp) { // Don't add duplicates const recentDupe = this.messages.find(m => m.sender === sender && m.text === text && Math.abs(m.timestamp - timestamp) < 1000 ); if (recentDupe) return; this.addMessage(sender, text, false, timestamp); // Increment unread if chat is closed if (!this.isOpen) { this.unreadCount++; this.updateUnreadBadge(); const toggle = document.getElementById('world-chat-toggle'); if (toggle) toggle.classList.add('has-unread'); } }, addMessage(sender, text, isSelf = false, timestamp = Date.now()) { const message = { sender, text, timestamp, isSelf }; this.messages.push(message); // Limit message history if (this.messages.length > this.maxMessages) { this.messages.shift(); } // Render message this.renderMessage(message); }, renderMessage(msg) { const container = document.getElementById('world-chat-messages'); if (!container) return; const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); const msgClass = msg.isSelf ? 'self' : ''; const msgEl = document.createElement('div'); msgEl.className = `world-chat-message ${msgClass}`; msgEl.innerHTML = `
${msg.sender}
${this.escapeHtml(msg.text)}
${time}
`; container.appendChild(msgEl); container.scrollTop = container.scrollHeight; }, addSystemMessage(text) { const container = document.getElementById('world-chat-messages'); if (!container) return; const msgEl = document.createElement('div'); msgEl.className = 'world-chat-message system'; msgEl.innerHTML = `
System
${text}
`; container.appendChild(msgEl); container.scrollTop = container.scrollHeight; }, updateOnlineCount(count) { const el = document.getElementById('world-chat-online'); if (el) el.textContent = `${count} online`; }, updateUnreadBadge() { const badge = document.getElementById('world-chat-unread'); if (badge) { badge.textContent = this.unreadCount > 9 ? '9+' : this.unreadCount; if (this.unreadCount > 0) { badge.classList.add('active'); } else { badge.classList.remove('active'); } } }, escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } }; // ============================================ // v7.1: QUICK EMOTE SYSTEM (Round 2 Consensus - 7/10 Impact) // Radial emote wheel for multiplayer expression // Hold G to open, click emote to send, release to close // ============================================ const EmoteSystem = { isOpen: false, cooldown: 0, COOLDOWN_TIME: 2000, // 2 seconds between emotes EMOTES: { wave: { icon: '👋', name: 'Wave', color: 0x00ff88 }, laugh: { icon: '😂', name: 'Laugh', color: 0xffff00 }, thumbsup: { icon: '👍', name: 'Nice', color: 0x00aaff }, celebrate: { icon: '🎉', name: 'Celebrate', color: 0xff00ff }, mind: { icon: '🤯', name: 'Wow', color: 0xff8800 }, salute: { icon: '🫡', name: 'Salute', color: 0x00ffff }, fire: { icon: '🔥', name: 'Fire', color: 0xff4400 }, heart: { icon: '❤️', name: 'Love', color: 0xff0066 } }, init() { // Hold G to show emote wheel document.addEventListener('keydown', (e) => { if (e.key === 'g' || e.key === 'G') { if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') { return; } if (!this.isOpen) { this.open(); } } }); document.addEventListener('keyup', (e) => { if (e.key === 'g' || e.key === 'G') { this.close(); } }); console.log('[EMOTE SYSTEM] Initialized'); }, open() { this.isOpen = true; const container = document.getElementById('emote-wheel-container'); if (container) container.classList.add('active'); }, close() { this.isOpen = false; const container = document.getElementById('emote-wheel-container'); if (container) container.classList.remove('active'); }, send(emoteId) { const now = Date.now(); if (now < this.cooldown) { showNotification('Emote on cooldown!', 'warning'); return; } const emote = this.EMOTES[emoteId]; if (!emote) return; this.cooldown = now + this.COOLDOWN_TIME; // Show emote locally this.displayLocalEmote(emote); // Broadcast to other players this.broadcast(emoteId); // Close wheel after sending this.close(); }, broadcast(emoteId) { // Use existing P2P system if (typeof broadcastDelta === 'function') { broadcastDelta({ type: 'emote', emoteId: emoteId, timestamp: Date.now() }); } // Also broadcast via PublicWorldManager if active if (window.PublicWorldManager && PublicWorldManager.isHost && PublicWorldManager.participants) { PublicWorldManager.participants.forEach(conn => { if (conn && conn.open) { conn.send({ type: 'PLAYER_EMOTE', emoteId: emoteId, timestamp: Date.now() }); } }); } else if (window.PublicWorldManager && PublicWorldManager.hostConnection) { PublicWorldManager.hostConnection.send({ type: 'PLAYER_EMOTE', emoteId: emoteId, timestamp: Date.now() }); } }, receiveEmote(peerId, emoteId) { const emote = this.EMOTES[emoteId]; if (!emote) return; // Display floating emote above remote player (or center screen if no position) this.displayRemoteEmote(peerId, emote); }, displayLocalEmote(emote) { // Create floating emote above player position const floater = document.createElement('div'); floater.className = 'floating-emote'; floater.textContent = emote.icon; floater.style.left = '50%'; floater.style.top = '40%'; floater.style.transform = 'translateX(-50%)'; document.body.appendChild(floater); // Remove after animation setTimeout(() => floater.remove(), 2000); // Also show in chat if (window.WorldChat) { WorldChat.addSystemMessage(`You used ${emote.icon} ${emote.name}`); } }, displayRemoteEmote(peerId, emote) { // Show notification for remote emote showNotification(`${emote.icon} ${emote.name}`, 'info'); // Create floating emote (offset slightly) const floater = document.createElement('div'); floater.className = 'floating-emote'; floater.textContent = emote.icon; floater.style.left = `${45 + Math.random() * 10}%`; floater.style.top = '35%'; document.body.appendChild(floater); setTimeout(() => floater.remove(), 2000); } }; // ============================================ // v7.1: ENCOUNTERED PLAYERS LEADERBOARD (Round 2 Consensus - 8/10 Impact) // Tracks real players encountered via multiplayer // Blends with simulated leaderboard for richer competition // ============================================ const EncounteredPlayers = { STORAGE_KEY: 'leviathan-encountered-players-v1', players: {}, // v8.0: Using SafeJSON for encountered players (8-Strategy Consensus Cycle 8) init() { const saved = SafeJSON.fromLocalStorage(this.STORAGE_KEY, null); if (saved) { this.players = saved; } // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[ENCOUNTERED PLAYERS] Loaded ${Object.keys(this.players).length} players`); }, save() { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.players)); }, // Record a player we encountered in multiplayer recordPlayer(peerId, playerData) { if (!peerId || !playerData) return; const existing = this.players[peerId]; const now = Date.now(); this.players[peerId] = { name: playerData.name || playerData.playerName || `Player_${peerId.substring(0, 6)}`, points: playerData.points || playerData.totalPoints || calculatePlayerPoints?.() || 0, rank: playerData.rank || playerData.playerRank?.lastTitle || 'Unknown', lastSeen: now, encounters: (existing?.encounters || 0) + 1, isReal: true, // Flag to distinguish from simulated worldId: playerData.worldId || PublicWorldManager?.currentWorld || 'unknown' }; this.save(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[ENCOUNTERED PLAYERS] Recorded: ${this.players[peerId].name} (${this.players[peerId].points} pts)`); }, // Get combined leaderboard (real + simulated) getEnhancedLeaderboard() { const realPlayers = Object.values(this.players) .filter(p => Date.now() - p.lastSeen < 30 * 24 * 60 * 60 * 1000) // Only players seen in last 30 days .map(p => ({ name: p.name, points: p.points, rank: p.rank, isReal: true, encounters: p.encounters })); // Get simulated players (if SIMULATED_PLAYERS exists) const simulated = (typeof SIMULATED_PLAYERS !== 'undefined' ? SIMULATED_PLAYERS : []) .map(p => ({ ...p, isReal: false })); // Add current player const myPoints = typeof calculatePlayerPoints === 'function' ? calculatePlayerPoints() : 0; const myRank = typeof getPlayerRank === 'function' ? getPlayerRank().title : 'Explorer'; const allPlayers = [ ...realPlayers, ...simulated, { name: 'YOU', points: myPoints, rank: myRank, isReal: true, isYou: true } ]; // Sort by points descending allPlayers.sort((a, b) => b.points - a.points); return allPlayers; }, // Get player's position in enhanced leaderboard getPosition() { const leaderboard = this.getEnhancedLeaderboard(); const myIndex = leaderboard.findIndex(p => p.isYou); return { position: myIndex + 1, total: leaderboard.length, realPlayerCount: leaderboard.filter(p => p.isReal && !p.isYou).length, nearby: leaderboard.slice(Math.max(0, myIndex - 2), myIndex + 3) }; }, // Clear old player data (for privacy) clearOldData(daysOld = 30) { const cutoff = Date.now() - (daysOld * 24 * 60 * 60 * 1000); let removed = 0; for (const peerId in this.players) { if (this.players[peerId].lastSeen < cutoff) { delete this.players[peerId]; removed++; } } if (removed > 0) { this.save(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[ENCOUNTERED PLAYERS] Cleared ${removed} old entries`); } } }; // ============================================ // v6.36: KILL REPLAY FLASH SYSTEM // Brief slow-mo replay of significant kills // Consensus feature from Round 3 strategy analysis // ============================================ const killReplay = { enabled: true, replayQueue: [], isReplaying: false, significantKillThreshold: 15, // Only replay kills above this damage cooldown: 0, cooldownTime: 8000, // Min time between replays recordKill(enemyType, damage, position, killerCombo) { if (!this.enabled || this.isReplaying) return; const now = Date.now(); if (now < this.cooldown) return; // Determine if this kill is replay-worthy const isSignificant = damage >= this.significantKillThreshold || killerCombo >= 20 || enemyType === 'boss' || enemyType === 'elite'; if (isSignificant) { this.triggerReplay(enemyType, damage, position, killerCombo); this.cooldown = now + this.cooldownTime; } }, triggerReplay(enemyType, damage, position, combo) { this.isReplaying = true; // Create replay flash overlay const overlay = document.createElement('div'); overlay.id = 'kill-replay-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: radial-gradient(circle, transparent 30%, rgba(255,0,0,0.3) 100%); z-index: 9999; pointer-events: none; animation: replayFlash 0.8s ease-out; `; document.body.appendChild(overlay); // Create kill text const killText = document.createElement('div'); killText.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 48px; font-weight: bold; color: #ff4444; text-shadow: 0 0 20px #ff0000, 0 0 40px #ff0000; z-index: 10000; animation: killTextPop 0.8s ease-out; pointer-events: none; white-space: nowrap; `; const killLabel = combo >= 20 ? `${combo}x COMBO KILL!` : enemyType === 'boss' ? 'BOSS SLAIN!' : damage >= 25 ? 'DEVASTATING!' : 'ELIMINATED!'; killText.textContent = killLabel; document.body.appendChild(killText); // Show damage number const damageNum = document.createElement('div'); damageNum.style.cssText = ` position: fixed; top: 58%; left: 50%; transform: translateX(-50%); font-size: 32px; color: #ffaa00; text-shadow: 0 0 10px #ff8800; z-index: 10000; animation: killTextPop 0.8s ease-out 0.1s both; pointer-events: none; `; damageNum.textContent = `-${damage} DMG`; document.body.appendChild(damageNum); // Slow-mo effect (if animation frame available) const originalTimeScale = window.gameTimeScale || 1; window.gameTimeScale = 0.3; // Play impact sound AudioSystem.playGentle(150, 0.3, 0.5); setTimeout(() => AudioSystem.playGentle(80, 0.4, 0.6), 100); // Cleanup setTimeout(() => { window.gameTimeScale = originalTimeScale; overlay.remove(); killText.remove(); damageNum.remove(); this.isReplaying = false; }, 800); } }; // Add CSS for replay animations const replayStyles = document.createElement('style'); replayStyles.textContent = ` @keyframes replayFlash { 0% { opacity: 1; transform: scale(1); } 50% { opacity: 0.8; } 100% { opacity: 0; transform: scale(1.1); } } @keyframes killTextPop { 0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; } 30% { transform: translate(-50%, -50%) scale(1.2); opacity: 1; } 100% { transform: translate(-50%, -50%) scale(1); opacity: 0; } } @keyframes recordPulse { 0% { transform: translate(-50%, -50%) scale(0.8); } 50% { transform: translate(-50%, -50%) scale(1.05); } 100% { transform: translate(-50%, -50%) scale(1); } } `; document.head.appendChild(replayStyles); // v5.15: Robot Animation Trigger System // v6.90: Extended with comprehensive ability casting animations // Triggers special animations on the explorer robot function triggerRobotAnimation(animType, options = {}) { if (!worldState.player || !worldState.player.userData.animation) return; const anim = worldState.player.userData.animation; switch (animType) { case 'attack': anim.attackPhase = 1; break; case 'damage': anim.damageFlash = 1; break; case 'wave': anim.wavePhase = 1; break; case 'jump': anim.jumpPhase = 1; break; case 'celebrate': // Celebrate is a wave + jump combo anim.wavePhase = 1; anim.jumpPhase = 0.8; break; // v6.90: Ability Casting Animations case 'powerStrike': // Powerful overhead slam - wind up then strike down anim.castType = 'powerStrike'; anim.castPhase = 1.0; anim.chargePhase = 1.0; anim.castIntensity = 1.0; anim.castGlow = 0.8; break; case 'whirlwind': // Spinning attack - arms out, full body rotation anim.castType = 'whirlwind'; anim.castPhase = 1.0; anim.spinPhase = 0; // Will increment in update loop anim.castIntensity = 1.0; anim.castGlow = 0.6; break; case 'warcry': // Chest thrust, head back, roar pose anim.castType = 'warcry'; anim.castPhase = 1.0; anim.chargePhase = 0.8; anim.castIntensity = 1.0; anim.castGlow = 0.7; anim.jumpPhase = 0.3; // Slight lift break; case 'heal': // Hands to chest, serene healing pose anim.castType = 'heal'; anim.castPhase = 1.0; anim.castIntensity = 0.8; anim.castGlow = 1.0; // Bright healing glow break; case 'dash': // Forward thrust pose with trailing motion anim.castType = 'dash'; anim.castPhase = 0.6; // Quick animation anim.jumpPhase = 0.5; anim.recoilPhase = 0.8; anim.castGlow = 0.5; break; case 'shieldWall': // Arms crossed in front, defensive stance anim.castType = 'shieldWall'; anim.castPhase = 1.0; anim.chargePhase = 0.6; anim.castIntensity = 0.7; anim.castGlow = 0.6; break; case 'execute': // Deadly precision strike - arm pulled back then thrust anim.castType = 'execute'; anim.castPhase = 1.0; anim.chargePhase = 0.9; anim.castIntensity = 1.0; anim.bodyTwist = 0.3; anim.castGlow = 0.9; break; case 'berserk': // Power-up pose - arms tensed, body vibrating with power anim.castType = 'berserk'; anim.castPhase = 1.0; anim.chargePhase = 1.0; anim.castIntensity = 1.2; // Extra intense anim.castGlow = 1.0; anim.jumpPhase = 0.4; break; case 'chronoEcho': // Mystical pose - arms out to sides, channeling time energy anim.castType = 'chronoEcho'; anim.castPhase = 1.0; anim.chargePhase = 0.7; anim.castIntensity = 0.9; anim.castGlow = 0.8; anim.wavePhase = 0.5; // Slight arm raise break; } } // ============================================ // v6.13: RIFT SURGE - Omniverse Gate Dash Effect // Channels energy from the dimensional rift to reshape reality // Creates an epic shockwave/force blast effect // ============================================ let leviathanPulseEffects = []; // v7.92: Optimized to use _abilityVec3Pool for position calculations function createFusRoDahEffect(startPos, direction, distance) { if (!scene) return; // Create multiple expanding rings that travel along dash path const ringCount = 5; const ringSpacing = distance / ringCount; for (let i = 0; i < ringCount; i++) { // v7.92: Use pooled vectors instead of clone() _abilityVec3Pool._temp1.copy(direction).multiplyScalar(i * ringSpacing); _abilityVec3Pool._temp2.copy(startPos).add(_abilityVec3Pool._temp1); _abilityVec3Pool._temp2.y += 1; // Create expanding ring geometry const ringGeo = new THREE.RingGeometry(0.5, 1.5, 32); const ringMat = new THREE.MeshBasicMaterial({ color: 0x88ffff, transparent: true, opacity: 0.8 - i * 0.1, side: THREE.DoubleSide }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.position.copy(_abilityVec3Pool._temp2); // Orient ring perpendicular to dash direction // v7.92: Use _temp1 for lookAt target _abilityVec3Pool._temp1.copy(ring.position).add(direction); ring.lookAt(_abilityVec3Pool._temp1); ring.userData = { createdAt: performance.now(), lifetime: 600, initialScale: 1 + i * 0.5, expandRate: 3 + i * 0.5, index: i }; scene.add(ring); leviathanPulseEffects.push(ring); } // Create central force cone/beam const coneGeo = new THREE.ConeGeometry(2, distance * 1.2, 16, 1, true); const coneMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.4, side: THREE.DoubleSide }); const cone = new THREE.Mesh(coneGeo, coneMat); // Position and orient cone // v7.92: Use pooled vectors instead of clone() _abilityVec3Pool._temp1.copy(direction).multiplyScalar(distance * 0.6); _abilityVec3Pool._temp2.copy(startPos).add(_abilityVec3Pool._temp1); _abilityVec3Pool._temp2.y += 1; cone.position.copy(_abilityVec3Pool._temp2); // Point cone in dash direction (rotate so tip faces forward) // v7.93: Use pooled quaternion instead of new Quaternion() per ability _abilityVec3Pool._tempQuaternion.setFromUnitVectors(_abilityVec3Pool._upVector, direction); cone.quaternion.copy(_abilityVec3Pool._tempQuaternion); cone.rotateX(Math.PI / 2); // Adjust so cone opens in direction of travel cone.userData = { createdAt: performance.now(), lifetime: 400, type: 'cone' }; scene.add(cone); leviathanPulseEffects.push(cone); // Create debris particles flying outward // v7.85: Use shared geometry pool to avoid 20 geometry allocations per ability use const debrisCount = 20; for (let i = 0; i < debrisCount; i++) { const debrisMat = new THREE.MeshBasicMaterial({ color: Math.random() > 0.5 ? 0x88ffff : 0xffffff, transparent: true, opacity: 0.9 }); const debris = new THREE.Mesh(_effectGeometryPool.debrisMedium, debrisMat); // Random position along path // v7.92: Use pooled vectors instead of clone() const t = Math.random(); _abilityVec3Pool._temp1.copy(direction).multiplyScalar(t * distance); _abilityVec3Pool._temp2.copy(startPos).add(_abilityVec3Pool._temp1); _abilityVec3Pool._temp2.y += 0.5 + Math.random() * 2; debris.position.copy(_abilityVec3Pool._temp2); // Random velocity perpendicular to dash direction const perpX = direction.z; const perpZ = -direction.x; const lateralVel = (Math.random() - 0.5) * 8; const upVel = 2 + Math.random() * 4; // v7.93: Store velocity/rotationSpeed as x/y/z primitives to avoid Vector3 allocation debris.userData = { createdAt: performance.now(), lifetime: 800, type: 'debris', vx: perpX * lateralVel + direction.x * 2, vy: upVel, vz: perpZ * lateralVel + direction.z * 2, rx: (Math.random() - 0.5) * 10, ry: (Math.random() - 0.5) * 10, rz: (Math.random() - 0.5) * 10 }; scene.add(debris); leviathanPulseEffects.push(debris); } // Create text floater with dramatic styling - v7.91: Use pooled position spawnFloater(getFloaterPos(startPos, 3), '💨 DASH! 💨', '#00ffff'); // Screen flash effect // v7.82: Use cached DOM reference to avoid getElementById per ability const flash = getUICache().damageOverlay; if (flash) { flash.style.background = 'radial-gradient(ellipse at center, rgba(136,255,255,0.5) 0%, transparent 70%)'; flash.style.opacity = '0.6'; setTimeout(() => { flash.style.background = 'linear-gradient(rgba(255,0,0,0.3), rgba(255,0,0,0))'; flash.style.opacity = '0'; }, 200); } } // Update Rift Surge effects each frame function updateFusRoDahEffects(dt) { const now = performance.now(); const toRemove = []; for (const effect of leviathanPulseEffects) { const age = now - effect.userData.createdAt; const progress = age / effect.userData.lifetime; if (progress >= 1) { toRemove.push(effect); continue; } if (effect.userData.type === 'debris') { // v7.93: Update debris physics using x/y/z primitives (no Vector3 access) const ud = effect.userData; effect.position.x += ud.vx * dt; effect.position.y += ud.vy * dt; effect.position.z += ud.vz * dt; ud.vy -= 15 * dt; // Gravity // Rotation effect.rotation.x += ud.rx * dt; effect.rotation.y += ud.ry * dt; effect.rotation.z += ud.rz * dt; // Fade out effect.material.opacity = 0.9 * (1 - progress); } else if (effect.userData.type === 'cone') { // Fade out cone effect.material.opacity = 0.4 * (1 - progress); effect.scale.setScalar(1 + progress * 0.5); } else { // Expanding rings const scale = effect.userData.initialScale + effect.userData.expandRate * progress; effect.scale.setScalar(scale); effect.material.opacity = (0.8 - effect.userData.index * 0.1) * (1 - progress); } } // Remove finished effects for (const effect of toRemove) { scene.remove(effect); effect.geometry?.dispose(); effect.material?.dispose(); } leviathanPulseEffects = leviathanPulseEffects.filter(e => !toRemove.includes(e)); } // ============================================ // v9.3: ICONIC ABILITY VISUAL EFFECTS // Each ability now has a unique, memorable visual signature // ============================================ let abilityEffects = []; // v7.85: Shared geometries for effect particles to avoid per-particle allocation // v7.90: Extended with fogRing and heatParticle geometries // v7.94: Added executeSlash (TorusGeometry) and deathMark (RingGeometry) for createExecuteEffect const _effectGeometryPool = { blood: null, // SphereGeometry(0.08, 4, 4) ember: null, // SphereGeometry(0.06, 4, 4) debrisSmall: null, // BoxGeometry(0.15, 0.15, 0.15) debrisMedium: null, // BoxGeometry(0.2, 0.2, 0.2) emberLarge: null, // SphereGeometry(0.1, 8, 8) fogRing: null, // v7.90: RingGeometry(1, 3, 32) for fog clearing heatParticle: null, // v7.90: SphereGeometry(0.15, 4, 4) for heat distortion executeSlash: null, // v7.94: TorusGeometry(3, 0.15, 8, 32, Math.PI) for execute ability deathMark: null, // v7.94: RingGeometry(1.5, 2, 32) for execute ability crossPlane: null, // v7.94: PlaneGeometry(0.2, 3) for execute cross lines shockRing: null, // v7.94: RingGeometry(0.5, 1.5, 32) for power strike shockwave sonicRing: null, // v7.94: RingGeometry(0.3, 0.8, 32) for warcry effect healHex: null, // v7.94: RingGeometry(1, 1.3, 6) for heal effect fireCircle: null, // v7.94: RingGeometry(2, 2.5, 32) for berserk effect berserkEmberRing: null, // v7.94: RingGeometry(0.5, 1, 32) for berserk expanding rings init() { this.blood = new THREE.SphereGeometry(0.08, 4, 4); this.ember = new THREE.SphereGeometry(0.06, 4, 4); this.debrisSmall = new THREE.BoxGeometry(0.15, 0.15, 0.15); this.debrisMedium = new THREE.BoxGeometry(0.2, 0.2, 0.2); this.emberLarge = new THREE.SphereGeometry(0.1, 8, 8); this.fogRing = new THREE.RingGeometry(1, 3, 32); this.heatParticle = new THREE.SphereGeometry(0.15, 4, 4); // v7.94: Pooled geometries for ability effects this.executeSlash = new THREE.TorusGeometry(3, 0.15, 8, 32, Math.PI); this.deathMark = new THREE.RingGeometry(1.5, 2, 32); this.crossPlane = new THREE.PlaneGeometry(0.2, 3); this.shockRing = new THREE.RingGeometry(0.5, 1.5, 32); this.sonicRing = new THREE.RingGeometry(0.3, 0.8, 32); this.healHex = new THREE.RingGeometry(1, 1.3, 6); this.fireCircle = new THREE.RingGeometry(2, 2.5, 32); this.berserkEmberRing = new THREE.RingGeometry(0.5, 1, 32); }, dispose() { if (this.blood) this.blood.dispose(); if (this.ember) this.ember.dispose(); if (this.debrisSmall) this.debrisSmall.dispose(); if (this.debrisMedium) this.debrisMedium.dispose(); if (this.emberLarge) this.emberLarge.dispose(); if (this.fogRing) this.fogRing.dispose(); if (this.heatParticle) this.heatParticle.dispose(); // v7.94: Dispose pooled ability geometries if (this.executeSlash) this.executeSlash.dispose(); if (this.deathMark) this.deathMark.dispose(); if (this.crossPlane) this.crossPlane.dispose(); if (this.shockRing) this.shockRing.dispose(); if (this.sonicRing) this.sonicRing.dispose(); if (this.healHex) this.healHex.dispose(); if (this.fireCircle) this.fireCircle.dispose(); if (this.berserkEmberRing) this.berserkEmberRing.dispose(); } }; _effectGeometryPool.init(); // v7.83: Vector3 pool for ability effects to reduce GC pressure // v7.84: Extended with _tempVelocity for hot path particle updates const _abilityVec3Pool = { pool: [], maxSize: 32, // Get a vector from pool or create new acquire() { if (this.pool.length > 0) { return this.pool.pop(); } return new THREE.Vector3(); }, // Return a vector to the pool release(vec) { if (this.pool.length < this.maxSize && vec) { vec.set(0, 0, 0); this.pool.push(vec); } }, // Pre-allocated temp vectors for common operations _temp1: null, _temp2: null, _upVector: null, _tempVelocity: null, // v7.84: For velocity * dt calculations in updateAbilityEffects _tempQuaternion: null, // v7.93: For cone orientation to avoid new Quaternion() per ability init() { this._temp1 = new THREE.Vector3(); this._temp2 = new THREE.Vector3(); this._upVector = new THREE.Vector3(0, 1, 0); this._tempVelocity = new THREE.Vector3(); // v7.84: Avoids clone() per particle per frame this._tempQuaternion = new THREE.Quaternion(); // v7.93: Reusable quaternion for setFromUnitVectors } }; // Initialize the pool _abilityVec3Pool.init(); // Power Strike: Massive forward slam with ground crack and fire eruption // v7.83: Uses vector pool to reduce GC pressure during ability effects // v7.94: Removed all clone() patterns, use pooled shockRing geometry function createPowerStrikeEffect(playerPos, direction) { if (!scene) return; // v7.94: Use pooled vectors instead of clone() for startPos const startPosX = playerPos.x; const startPosY = playerPos.y + 0.5; const startPosZ = playerPos.z; // Create ground crack lines emanating forward const crackCount = 7; for (let i = 0; i < crackCount; i++) { const angle = (i - 3) * 0.15; // Spread angle // v7.94: Use pooled _tempVelocity for axis rotation instead of clone() _abilityVec3Pool._tempVelocity.copy(direction).applyAxisAngle(_abilityVec3Pool._upVector, angle); const crackDirX = _abilityVec3Pool._tempVelocity.x; const crackDirZ = _abilityVec3Pool._tempVelocity.z; const crackLength = 4 + Math.random() * 3; const crackGeo = new THREE.PlaneGeometry(0.15, crackLength); const crackMat = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.9, side: THREE.DoubleSide }); const crack = new THREE.Mesh(crackGeo, crackMat); // v7.94: Calculate position using primitives instead of clone() const halfLen = crackLength / 2; crack.position.set( startPosX + crackDirX * halfLen, 0.02, startPosZ + crackDirZ * halfLen ); crack.rotation.x = -Math.PI / 2; // v7.83: Use pre-allocated up vector _abilityVec3Pool._temp2.copy(crack.position).add(_abilityVec3Pool._upVector); crack.lookAt(_abilityVec3Pool._temp2); crack.rotation.z = Math.atan2(crackDirX, crackDirZ); crack.userData = { createdAt: performance.now(), lifetime: 800, type: 'crack', index: i }; scene.add(crack); abilityEffects.push(crack); } // Fire eruption pillars along the strike for (let i = 0; i < 5; i++) { // v7.94: Calculate pillar position using primitives instead of clone() const mult = 1 + i * 1.2; const pillarGeo = new THREE.CylinderGeometry(0.3, 0.5, 3 + i * 0.5, 8, 1, true); const pillarMat = new THREE.MeshBasicMaterial({ color: i % 2 === 0 ? 0xff4400 : 0xffaa00, transparent: true, opacity: 0.7, side: THREE.DoubleSide }); const pillar = new THREE.Mesh(pillarGeo, pillarMat); pillar.position.set( startPosX + direction.x * mult, 0, startPosZ + direction.z * mult ); pillar.userData = { createdAt: performance.now() + i * 50, lifetime: 600, type: 'firePillar', delay: i * 50, baseY: 0, maxHeight: 3 + i * 0.5 }; scene.add(pillar); abilityEffects.push(pillar); } // Impact shockwave ring - v7.94: Use pooled shockRing geometry const ringMat = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.8, side: THREE.DoubleSide }); const ring = new THREE.Mesh(_effectGeometryPool.shockRing, ringMat); ring.position.set(startPosX, 0.1, startPosZ); ring.rotation.x = -Math.PI / 2; ring.userData = { createdAt: performance.now(), lifetime: 500, type: 'shockRing', expandRate: 8, usesPooledGeo: true }; scene.add(ring); abilityEffects.push(ring); // Screen flash // v7.82: Use cached DOM reference to avoid getElementById per ability const flash = getUICache().damageOverlay; if (flash) { flash.style.background = 'radial-gradient(ellipse at center, rgba(255,68,0,0.6) 0%, transparent 60%)'; flash.style.opacity = '0.7'; setTimeout(() => { flash.style.background = ''; flash.style.opacity = '0'; }, 150); } } // Whirlwind: Tornado vortex with swirling debris // v7.94: Removed clone() - use primitives for centerPos function createWhirlwindEffect(playerPos) { if (!scene) return; // v7.94: Store as primitives to avoid clone() const centerX = playerPos.x; const centerY = playerPos.y + 1; const centerZ = playerPos.z; // Create spiral tornado cone const spiralCount = 3; for (let s = 0; s < spiralCount; s++) { const spiralGeo = new THREE.ConeGeometry(2.5 - s * 0.3, 4 + s * 0.5, 16, 1, true); const spiralMat = new THREE.MeshBasicMaterial({ color: s === 0 ? 0x00ffff : (s === 1 ? 0x44aaff : 0x8888ff), transparent: true, opacity: 0.3 - s * 0.05, side: THREE.DoubleSide, wireframe: s > 0 }); const spiral = new THREE.Mesh(spiralGeo, spiralMat); // v7.94: Use primitives instead of centerPos.copy() spiral.position.set(centerX, centerY + s * 0.3, centerZ); spiral.userData = { createdAt: performance.now(), lifetime: 1000, type: 'tornado', rotationSpeed: 8 + s * 2, layer: s }; scene.add(spiral); abilityEffects.push(spiral); } // Swirling debris particles // v7.85: Use shared geometry pool to avoid 25 geometry allocations per ability use const debrisCount = 25; for (let i = 0; i < debrisCount; i++) { const angle = (i / debrisCount) * Math.PI * 2; const radius = 1 + Math.random() * 2; const height = Math.random() * 4; const debrisMat = new THREE.MeshBasicMaterial({ color: Math.random() > 0.5 ? 0x00ffff : 0xffffff, transparent: true, opacity: 0.8 }); const debris = new THREE.Mesh(_effectGeometryPool.debrisSmall, debrisMat); // v7.94: Use primitives directly debris.position.set( centerX + Math.cos(angle) * radius, centerY + height, centerZ + Math.sin(angle) * radius ); debris.userData = { createdAt: performance.now(), lifetime: 1000, type: 'whirlDebris', angle: angle, radius: radius, height: height, // v7.92: Store x,y,z directly instead of clone() to avoid allocation centerX: centerX, centerY: centerY, centerZ: centerZ, orbitSpeed: 6 + Math.random() * 4, riseSpeed: 2 + Math.random() * 2 }; scene.add(debris); abilityEffects.push(debris); } // Horizontal wind rings for (let i = 0; i < 4; i++) { const ringGeo = new THREE.TorusGeometry(2, 0.1, 8, 32); const ringMat = new THREE.MeshBasicMaterial({ color: 0x00ddff, transparent: true, opacity: 0.4 }); const ring = new THREE.Mesh(ringGeo, ringMat); // v7.94: Use primitives instead of centerPos.copy() ring.position.set(centerX, 0.5 + i * 1, centerZ); ring.rotation.x = Math.PI / 2; ring.userData = { createdAt: performance.now() + i * 100, lifetime: 800, type: 'windRing', delay: i * 100, baseY: 0.5 + i * 1, expandRate: 3 }; scene.add(ring); abilityEffects.push(ring); } // Screen effect // v7.82: Use cached DOM reference to avoid getElementById per ability const container = getUICache().gameContainer; if (container) { container.style.boxShadow = 'inset 0 0 80px rgba(0, 255, 255, 0.4)'; setTimeout(() => { container.style.boxShadow = ''; }, 400); } } // Warcry: Sonic boom with radiating power waves // v7.94: Removed clone() - use primitives for centerPos, use pooled sonicRing geometry function createWarcryEffect(playerPos) { if (!scene) return; // v7.94: Store as primitives to avoid clone() const centerX = playerPos.x; const centerY = playerPos.y + 1.5; const centerZ = playerPos.z; // Create expanding sonic rings - v7.94: Use pooled sonicRing geometry const ringCount = 5; for (let i = 0; i < ringCount; i++) { const ringMat = new THREE.MeshBasicMaterial({ color: 0xff8800, transparent: true, opacity: 0.8 - i * 0.1, side: THREE.DoubleSide }); const ring = new THREE.Mesh(_effectGeometryPool.sonicRing, ringMat); ring.position.set(centerX, centerY, centerZ); // v7.94: Store random axis as primitives instead of new Vector3() const axisX = (Math.random() - 0.5) * 0.3; const axisY = 1; const axisZ = (Math.random() - 0.5) * 0.3; const axisLen = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); ring.userData = { createdAt: performance.now() + i * 80, lifetime: 600, type: 'sonicRing', delay: i * 80, expandRate: 6, usesPooledGeo: true, // v7.94: Store axis as primitives axisX: axisX / axisLen, axisY: axisY / axisLen, axisZ: axisZ / axisLen }; // v7.92: Use _abilityVec3Pool._temp1 instead of clone() for lookAt target _abilityVec3Pool._temp1.set( centerX + ring.userData.axisX, centerY + ring.userData.axisY, centerZ + ring.userData.axisZ ); ring.lookAt(_abilityVec3Pool._temp1); scene.add(ring); abilityEffects.push(ring); } // Power aura flames const flameCount = 12; for (let i = 0; i < flameCount; i++) { const angle = (i / flameCount) * Math.PI * 2; const flameGeo = new THREE.ConeGeometry(0.2, 1.5, 6); const flameMat = new THREE.MeshBasicMaterial({ color: i % 2 === 0 ? 0xff6600 : 0xffaa00, transparent: true, opacity: 0.7 }); const flame = new THREE.Mesh(flameGeo, flameMat); // v7.94: Use primitives directly const flameX = centerX + Math.cos(angle) * 1.2; const flameY = centerY - 0.5; const flameZ = centerZ + Math.sin(angle) * 1.2; flame.position.set(flameX, flameY, flameZ); flame.userData = { createdAt: performance.now(), lifetime: 800, type: 'auraFlame', angle: angle, // v7.92: Store x,y,z directly instead of clone() to avoid allocation basePosX: flameX, basePosY: flameY, basePosZ: flameZ }; scene.add(flame); abilityEffects.push(flame); } // Ground impact circle const groundGeo = new THREE.CircleGeometry(3, 32); const groundMat = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.5, side: THREE.DoubleSide }); const ground = new THREE.Mesh(groundGeo, groundMat); // v7.94: Use primitives directly ground.position.set(playerPos.x, 0.05, playerPos.z); ground.rotation.x = -Math.PI / 2; ground.userData = { createdAt: performance.now(), lifetime: 1000, type: 'groundGlow' }; scene.add(ground); abilityEffects.push(ground); // Screen shake color // v7.82: Use cached DOM reference to avoid getElementById per ability const flash = getUICache().damageOverlay; if (flash) { flash.style.background = 'radial-gradient(ellipse at center, rgba(255,136,0,0.5) 0%, transparent 70%)'; flash.style.opacity = '0.6'; setTimeout(() => { flash.style.background = ''; flash.style.opacity = '0'; }, 200); } } // Heal: Sacred light column with healing particles // v7.94: Removed clone() - use primitives for centerPos, use pooled healHex geometry function createHealEffect(playerPos) { if (!scene) return; // v7.94: Store as primitives to avoid clone() const centerX = playerPos.x; const centerY = playerPos.y; const centerZ = playerPos.z; // Light column from above const columnGeo = new THREE.CylinderGeometry(1.5, 2, 15, 16, 1, true); const columnMat = new THREE.MeshBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.3, side: THREE.DoubleSide }); const column = new THREE.Mesh(columnGeo, columnMat); // v7.94: Use primitives directly column.position.set(centerX, 7, centerZ); column.userData = { createdAt: performance.now(), lifetime: 1200, type: 'healColumn' }; scene.add(column); abilityEffects.push(column); // Inner sacred geometry - v7.94: Use pooled healHex geometry const hexMat = new THREE.MeshBasicMaterial({ color: 0x88ffaa, transparent: true, opacity: 0.6, side: THREE.DoubleSide }); const hex = new THREE.Mesh(_effectGeometryPool.healHex, hexMat); // v7.94: Use primitives directly hex.position.set(centerX, 0.1, centerZ); hex.rotation.x = -Math.PI / 2; hex.userData = { createdAt: performance.now(), lifetime: 1200, type: 'healHex', rotationSpeed: 2, usesPooledGeo: true }; scene.add(hex); abilityEffects.push(hex); // Rising healing sparkles const sparkleCount = 30; for (let i = 0; i < sparkleCount; i++) { const angle = Math.random() * Math.PI * 2; const radius = Math.random() * 2; const sparkleGeo = new THREE.SphereGeometry(0.08, 6, 6); const sparkleMat = new THREE.MeshBasicMaterial({ color: Math.random() > 0.3 ? 0x00ff88 : 0xffffff, transparent: true, opacity: 0.9 }); const sparkle = new THREE.Mesh(sparkleGeo, sparkleMat); // v7.94: Use primitives directly sparkle.position.set( centerX + Math.cos(angle) * radius, centerY + Math.random() * 0.5, centerZ + Math.sin(angle) * radius ); sparkle.userData = { createdAt: performance.now() + Math.random() * 300, lifetime: 1000, type: 'healSparkle', riseSpeed: 3 + Math.random() * 2, wobble: Math.random() * Math.PI * 2 }; scene.add(sparkle); abilityEffects.push(sparkle); } // Screen glow // v7.82: Use cached DOM reference to avoid getElementById per ability const container = getUICache().gameContainer; if (container) { container.style.boxShadow = 'inset 0 0 100px rgba(0, 255, 136, 0.4)'; setTimeout(() => { container.style.boxShadow = ''; }, 600); } } // Shield Wall: Hexagonal energy barrier formation // v7.94: Removed clone() - use primitives for centerPos function createShieldWallEffect(playerPos) { if (!scene) return; // v7.94: Store as primitives to avoid clone() const centerX = playerPos.x; const centerY = playerPos.y + 1; const centerZ = playerPos.z; // Create hexagonal shield panels in a dome const panelCount = 7; for (let i = 0; i < panelCount; i++) { const angle = (i / panelCount) * Math.PI * 2; const panelGeo = new THREE.CircleGeometry(0.8, 6); const panelMat = new THREE.MeshBasicMaterial({ color: 0x4488ff, transparent: true, opacity: 0.5, side: THREE.DoubleSide }); const panel = new THREE.Mesh(panelGeo, panelMat); // v7.94: Use primitives directly panel.position.set( centerX + Math.cos(angle) * 1.8, centerY, centerZ + Math.sin(angle) * 1.8 ); // v7.94: Use pooled _temp1 for lookAt instead of centerPos _abilityVec3Pool._temp1.set(centerX, centerY, centerZ); panel.lookAt(_abilityVec3Pool._temp1); panel.userData = { createdAt: performance.now() + i * 50, lifetime: 1500, type: 'shieldPanel', delay: i * 50, index: i }; scene.add(panel); abilityEffects.push(panel); } // Energy connection lines between panels const lineCount = panelCount; for (let i = 0; i < lineCount; i++) { const angle1 = (i / panelCount) * Math.PI * 2; const angle2 = ((i + 1) / panelCount) * Math.PI * 2; const lineGeo = new THREE.BufferGeometry(); // v7.94: Use pooled vectors for line points _abilityVec3Pool._temp1.set(centerX + Math.cos(angle1) * 1.8, centerY, centerZ + Math.sin(angle1) * 1.8); _abilityVec3Pool._temp2.set(centerX + Math.cos(angle2) * 1.8, centerY, centerZ + Math.sin(angle2) * 1.8); const points = [_abilityVec3Pool._temp1.clone(), _abilityVec3Pool._temp2.clone()]; lineGeo.setFromPoints(points); const lineMat = new THREE.LineBasicMaterial({ color: 0x88bbff, transparent: true, opacity: 0.6 }); const line = new THREE.Line(lineGeo, lineMat); line.userData = { createdAt: performance.now() + 200, lifetime: 1300, type: 'shieldLine', delay: 200 }; scene.add(line); abilityEffects.push(line); } // Dome top const domeGeo = new THREE.SphereGeometry(2.2, 16, 8, 0, Math.PI * 2, 0, Math.PI / 2); const domeMat = new THREE.MeshBasicMaterial({ color: 0x4488ff, transparent: true, opacity: 0.15, side: THREE.DoubleSide, wireframe: true }); const dome = new THREE.Mesh(domeGeo, domeMat); // v7.94: Use primitives directly dome.position.set(centerX, centerY - 0.5, centerZ); dome.userData = { createdAt: performance.now(), lifetime: 1500, type: 'shieldDome' }; scene.add(dome); abilityEffects.push(dome); // Screen flash // v7.82: Use cached DOM reference to avoid getElementById per ability const flash = getUICache().damageOverlay; if (flash) { flash.style.background = 'radial-gradient(ellipse at center, rgba(68,136,255,0.5) 0%, transparent 70%)'; flash.style.opacity = '0.5'; setTimeout(() => { flash.style.background = ''; flash.style.opacity = '0'; }, 200); } } // Execute: Death mark and reaper slash effect // v7.93: Optimized to use pooled vectors instead of clone() patterns // v7.94: Use pooled geometries for TorusGeometry, RingGeometry, PlaneGeometry function createExecuteEffect(playerPos, direction) { if (!scene) return; // v7.93: Use pooled vector for startPos calculation _abilityVec3Pool._temp1.copy(playerPos); _abilityVec3Pool._temp1.y += 1; // Giant slash arc - v7.94: Use pooled executeSlash geometry const slashMat = new THREE.MeshBasicMaterial({ color: 0xff0044, transparent: true, opacity: 0.9, side: THREE.DoubleSide }); const slash = new THREE.Mesh(_effectGeometryPool.executeSlash, slashMat); // v7.93: Use pooled _temp2 for direction offset instead of clone() _abilityVec3Pool._temp2.copy(direction).multiplyScalar(2); slash.position.copy(_abilityVec3Pool._temp1).add(_abilityVec3Pool._temp2); // v7.93: Use _tempVelocity for lookAt target instead of clone() _abilityVec3Pool._tempVelocity.copy(slash.position).add(direction); slash.lookAt(_abilityVec3Pool._tempVelocity); slash.rotation.z = Math.PI / 4; slash.userData = { createdAt: performance.now(), lifetime: 400, type: 'executeSlash', usesPooledGeo: true }; scene.add(slash); abilityEffects.push(slash); // Death mark symbol on ground - v7.94: Use pooled deathMark geometry const markMat = new THREE.MeshBasicMaterial({ color: 0xff0044, transparent: true, opacity: 0.7, side: THREE.DoubleSide }); const mark = new THREE.Mesh(_effectGeometryPool.deathMark, markMat); // v7.93: Use pooled vector for direction offset instead of clone() _abilityVec3Pool._temp2.copy(direction).multiplyScalar(3); mark.position.copy(_abilityVec3Pool._temp1).add(_abilityVec3Pool._temp2); mark.position.y = 0.05; mark.rotation.x = -Math.PI / 2; mark.userData = { createdAt: performance.now(), lifetime: 800, type: 'deathMark', rotationSpeed: 3, usesPooledGeo: true }; scene.add(mark); abilityEffects.push(mark); // Inner cross - v7.94: Use pooled crossPlane geometry for (let i = 0; i < 2; i++) { const crossMat = new THREE.MeshBasicMaterial({ color: 0xff0044, transparent: true, opacity: 0.8, side: THREE.DoubleSide }); const cross = new THREE.Mesh(_effectGeometryPool.crossPlane, crossMat); cross.position.copy(mark.position); cross.position.y = 0.06; cross.rotation.x = -Math.PI / 2; cross.rotation.z = i * Math.PI / 2; cross.userData = { createdAt: performance.now(), lifetime: 800, type: 'crossLine', usesPooledGeo: true }; scene.add(cross); abilityEffects.push(cross); } // Blood particles // v7.85: Use shared geometry pool to avoid 20 geometry allocations per ability use // v7.93: Store velocity as x/y/z primitives to avoid Vector3 allocation per particle const bloodCount = 20; for (let i = 0; i < bloodCount; i++) { const bloodMat = new THREE.MeshBasicMaterial({ color: 0xff0044, transparent: true, opacity: 0.8 }); const blood = new THREE.Mesh(_effectGeometryPool.blood, bloodMat); // v7.93: Use pooled vector for direction offset instead of clone() const distOffset = 2 + Math.random() * 2; _abilityVec3Pool._temp2.copy(direction).multiplyScalar(distOffset); blood.position.copy(_abilityVec3Pool._temp1).add(_abilityVec3Pool._temp2); // v7.93: Store velocity as primitives blood.userData = { createdAt: performance.now(), lifetime: 600, type: 'bloodParticle', vx: (Math.random() - 0.5) * 4, vy: 2 + Math.random() * 3, vz: (Math.random() - 0.5) * 4 }; scene.add(blood); abilityEffects.push(blood); } // Screen flash - blood red // v7.82: Use cached DOM reference to avoid getElementById per ability const flash = getUICache().damageOverlay; if (flash) { flash.style.background = 'radial-gradient(ellipse at center, rgba(255,0,68,0.6) 0%, transparent 60%)'; flash.style.opacity = '0.7'; setTimeout(() => { flash.style.background = ''; flash.style.opacity = '0'; }, 150); } } // Berserk: Rage aura with pulsing power and flame crown // v7.94: Removed clone() - use primitives for centerPos, use pooled fireCircle and berserkEmberRing geometries function createBerserkEffect(playerPos) { if (!scene) return; // v7.94: Store as primitives to avoid clone() const centerX = playerPos.x; const centerY = playerPos.y; const centerZ = playerPos.z; // Rage aura sphere const auraGeo = new THREE.SphereGeometry(2, 16, 16); const auraMat = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.3, side: THREE.DoubleSide }); const aura = new THREE.Mesh(auraGeo, auraMat); // v7.94: Use primitives directly aura.position.set(centerX, centerY + 1, centerZ); aura.userData = { createdAt: performance.now(), lifetime: 1500, type: 'rageAura', pulseSpeed: 8 }; scene.add(aura); abilityEffects.push(aura); // Flame crown const crownCount = 8; for (let i = 0; i < crownCount; i++) { const angle = (i / crownCount) * Math.PI * 2; const flameGeo = new THREE.ConeGeometry(0.15, 0.8, 6); const flameMat = new THREE.MeshBasicMaterial({ color: i % 2 === 0 ? 0xff4400 : 0xffaa00, transparent: true, opacity: 0.8 }); const flame = new THREE.Mesh(flameGeo, flameMat); // v7.94: Use primitives directly const flameX = centerX + Math.cos(angle) * 0.5; const flameY = centerY + 2.5; const flameZ = centerZ + Math.sin(angle) * 0.5; flame.position.set(flameX, flameY, flameZ); flame.userData = { createdAt: performance.now(), lifetime: 1500, type: 'crownFlame', angle: angle, // v7.92: Store x,y,z directly instead of clone() to avoid allocation basePosX: flameX, basePosY: flameY, basePosZ: flameZ }; scene.add(flame); abilityEffects.push(flame); } // Ground fire circle - v7.94: Use pooled fireCircle geometry const fireCircleMat = new THREE.MeshBasicMaterial({ color: 0xff2200, transparent: true, opacity: 0.6, side: THREE.DoubleSide }); const fireCircle = new THREE.Mesh(_effectGeometryPool.fireCircle, fireCircleMat); // v7.94: Use primitives directly fireCircle.position.set(centerX, 0.05, centerZ); fireCircle.rotation.x = -Math.PI / 2; fireCircle.userData = { createdAt: performance.now(), lifetime: 1500, type: 'fireCircle', usesPooledGeo: true }; scene.add(fireCircle); abilityEffects.push(fireCircle); // Rising embers // v7.85: Use shared geometry pool to avoid 30 geometry allocations per ability use const emberCount = 30; for (let i = 0; i < emberCount; i++) { const angle = Math.random() * Math.PI * 2; const radius = 0.5 + Math.random() * 2; const emberMat = new THREE.MeshBasicMaterial({ color: Math.random() > 0.5 ? 0xff4400 : 0xffaa00, transparent: true, opacity: 0.9 }); const ember = new THREE.Mesh(_effectGeometryPool.ember, emberMat); // v7.94: Use primitives directly ember.position.set( centerX + Math.cos(angle) * radius, centerY, centerZ + Math.sin(angle) * radius ); ember.userData = { createdAt: performance.now() + Math.random() * 500, lifetime: 1200, type: 'ember', riseSpeed: 2 + Math.random() * 3, wobble: Math.random() * Math.PI * 2, wobbleSpeed: 3 + Math.random() * 2 }; scene.add(ember); abilityEffects.push(ember); } // Explosive outward rings - v7.94: Use pooled berserkEmberRing geometry for (let i = 0; i < 3; i++) { const ringMat = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.7 - i * 0.15, side: THREE.DoubleSide }); const ring = new THREE.Mesh(_effectGeometryPool.berserkEmberRing, ringMat); // v7.94: Use primitives directly ring.position.set(centerX, centerY + 1, centerZ); ring.userData = { createdAt: performance.now() + i * 100, lifetime: 600, type: 'berserkRing', usesPooledGeo: true, delay: i * 100, expandRate: 5 }; scene.add(ring); abilityEffects.push(ring); } // Screen effect - intense red // v7.82: Use cached DOM reference to avoid getElementById per ability const container = getUICache().gameContainer; if (container) { container.style.boxShadow = 'inset 0 0 120px rgba(255,68,0,0.5)'; setTimeout(() => { container.style.boxShadow = ''; }, 500); } } // Update all ability effects each frame function updateAbilityEffects(dt) { const now = performance.now(); const toRemove = []; for (const effect of abilityEffects) { const delay = effect.userData.delay || 0; const age = now - effect.userData.createdAt - delay; if (age < 0) continue; // Still waiting for delay const progress = age / effect.userData.lifetime; if (progress >= 1) { toRemove.push(effect); continue; } // Update based on effect type switch (effect.userData.type) { case 'crack': effect.material.opacity = 0.9 * (1 - progress); effect.scale.x = 1 + progress * 0.5; break; case 'firePillar': const pillarPhase = Math.min(progress * 3, 1); effect.scale.y = pillarPhase; effect.position.y = effect.userData.baseY + effect.userData.maxHeight * pillarPhase * 0.5; effect.material.opacity = 0.7 * (1 - progress * 0.5); break; case 'shockRing': effect.scale.setScalar(1 + effect.userData.expandRate * progress); effect.material.opacity = 0.8 * (1 - progress); break; case 'tornado': effect.rotation.y += effect.userData.rotationSpeed * dt; effect.scale.setScalar(1 + progress * 0.3); effect.material.opacity = (0.3 - effect.userData.layer * 0.05) * (1 - progress); break; case 'whirlDebris': effect.userData.angle += effect.userData.orbitSpeed * dt; effect.userData.height += effect.userData.riseSpeed * dt; // v7.92: Use centerX/Y/Z instead of center.x/y/z to avoid clone effect.position.x = effect.userData.centerX + Math.cos(effect.userData.angle) * effect.userData.radius; effect.position.z = effect.userData.centerZ + Math.sin(effect.userData.angle) * effect.userData.radius; effect.position.y = effect.userData.centerY + effect.userData.height; effect.rotation.x += 5 * dt; effect.rotation.y += 5 * dt; effect.material.opacity = 0.8 * (1 - progress); break; case 'windRing': effect.scale.setScalar(1 + effect.userData.expandRate * progress); effect.position.y = effect.userData.baseY + progress * 2; effect.material.opacity = 0.4 * (1 - progress); break; case 'sonicRing': effect.scale.setScalar(1 + effect.userData.expandRate * progress); effect.material.opacity = (0.8 - effect.userData.index * 0.1) * (1 - progress); break; case 'auraFlame': const flameWave = Math.sin(now * 0.01 + effect.userData.angle) * 0.2; // v7.92: Use basePosY instead of basePos.y to avoid clone effect.position.y = effect.userData.basePosY + flameWave; effect.scale.y = 1 + flameWave; effect.material.opacity = 0.7 * (1 - progress * 0.3); break; case 'groundGlow': effect.material.opacity = 0.5 * (1 - progress); effect.scale.setScalar(1 + progress * 0.5); break; case 'healColumn': effect.material.opacity = 0.3 * (1 - progress * 0.5); effect.scale.x = 1 + Math.sin(progress * Math.PI) * 0.3; effect.scale.z = 1 + Math.sin(progress * Math.PI) * 0.3; break; case 'healHex': effect.rotation.z += effect.userData.rotationSpeed * dt; effect.scale.setScalar(1 + progress * 0.5); effect.material.opacity = 0.6 * (1 - progress * 0.5); break; case 'healSparkle': effect.position.y += effect.userData.riseSpeed * dt; effect.position.x += Math.sin(now * 0.01 + effect.userData.wobble) * 0.02; effect.material.opacity = 0.9 * (1 - progress); break; case 'shieldPanel': const panelPulse = 0.5 + Math.sin(now * 0.008 + effect.userData.index) * 0.2; effect.material.opacity = panelPulse * (1 - progress * 0.3); break; case 'shieldLine': effect.material.opacity = 0.6 * (1 - progress * 0.5); break; case 'shieldDome': effect.rotation.y += 0.5 * dt; effect.material.opacity = 0.15 * (1 - progress * 0.5); break; case 'executeSlash': effect.scale.setScalar(1 + progress * 2); effect.material.opacity = 0.9 * (1 - progress); break; case 'deathMark': effect.rotation.z += effect.userData.rotationSpeed * dt; effect.material.opacity = 0.7 * (1 - progress); break; case 'crossLine': effect.material.opacity = 0.8 * (1 - progress); break; case 'bloodParticle': // v7.93: Use x/y/z primitives for velocity (no Vector3 access) const bud = effect.userData; effect.position.x += bud.vx * dt; effect.position.y += bud.vy * dt; effect.position.z += bud.vz * dt; bud.vy -= 10 * dt; // Gravity effect.material.opacity = 0.8 * (1 - progress); break; case 'rageAura': const pulse = 1 + Math.sin(now * 0.001 * effect.userData.pulseSpeed) * 0.2; effect.scale.setScalar(pulse); effect.material.opacity = 0.3 * (1 - progress * 0.3); break; case 'crownFlame': const crownWave = Math.sin(now * 0.015 + effect.userData.angle * 2) * 0.15; // v7.92: Use basePosY instead of basePos.y to avoid clone effect.position.y = effect.userData.basePosY + crownWave; effect.scale.y = 1 + Math.abs(crownWave); effect.material.opacity = 0.8 * (1 - progress * 0.3); break; case 'fireCircle': const firePulse = 1 + Math.sin(now * 0.01) * 0.1; effect.scale.setScalar(firePulse); effect.material.opacity = 0.6 * (1 - progress); break; case 'ember': effect.position.y += effect.userData.riseSpeed * dt; effect.position.x += Math.sin(now * 0.001 * effect.userData.wobbleSpeed + effect.userData.wobble) * 0.03; effect.material.opacity = 0.9 * (1 - progress); break; case 'berserkRing': effect.scale.setScalar(1 + effect.userData.expandRate * progress); effect.material.opacity = (0.7 - effect.userData.index * 0.15) * (1 - progress); break; } } // Remove finished effects for (const effect of toRemove) { scene.remove(effect); effect.geometry?.dispose(); effect.material?.dispose(); } abilityEffects = abilityEffects.filter(e => !toRemove.includes(e)); } // ============================================ // v6.16: FOG CLEARING EFFECT // Thermal shockwave from dash disperses nearby fog // Creates a temporary clear zone that slowly refills // ============================================ let fogClearEffects = []; let fogClearActive = false; let fogClearStartTime = 0; const FOG_CLEAR_DURATION = 8000; // 8 seconds of clear visibility const FOG_CLEAR_FADE_TIME = 3000; // 3 seconds to fade back // v7.90: Pre-allocated temp vectors for fog clearing effect calculations let _fogClearTemp = null; let _fogClearLookAt = null; function createFogClearingEffect(startPos, direction, distance) { if (!scene || !scene.fog) return; // v7.90: Lazy-init temp vectors if (!_fogClearTemp) _fogClearTemp = new THREE.Vector3(); if (!_fogClearLookAt) _fogClearLookAt = new THREE.Vector3(0, 1, 0); fogClearActive = true; fogClearStartTime = performance.now(); // Store original fog values const originalNear = scene.fog.near; const originalFar = scene.fog.far; // Immediately push fog back significantly scene.fog.near = 60; scene.fog.far = 200; // Create visual fog dispersal effect - expanding thermal rings // v7.90: Use pooled geometry and temp vectors to avoid per-ring allocations const ringCount = 4; for (let i = 0; i < ringCount; i++) { // v7.90: Calculate ring position without clone() calls const ringPosX = startPos.x + direction.x * (i * distance / ringCount); const ringPosY = startPos.y + direction.y * (i * distance / ringCount) + 1.5; const ringPosZ = startPos.z + direction.z * (i * distance / ringCount); // White/yellow thermal wave rings // v7.90: Use pooled fogRing geometry instead of creating new per ring const ringMat = new THREE.MeshBasicMaterial({ color: 0xffffaa, transparent: true, opacity: 0.5 - i * 0.1, side: THREE.DoubleSide }); const ring = new THREE.Mesh(_effectGeometryPool.fogRing, ringMat); ring.position.set(ringPosX, ringPosY, ringPosZ); // v7.90: Use temp vector for lookAt calculation _fogClearTemp.set(ringPosX, ringPosY + 1, ringPosZ); ring.lookAt(_fogClearTemp); ring.rotation.x = Math.PI / 2; // Horizontal rings ring.userData = { createdAt: performance.now(), lifetime: 1500, initialScale: 1 + i * 0.8, expandRate: 8, type: 'fogRing', usesPooledGeo: true // v7.90: Flag to skip dispose on pooled geometry }; scene.add(ring); fogClearEffects.push(ring); } // Create rising heat distortion particles // v7.90: Use pooled heatParticle geometry and store velocity as plain object const particleCount = 30; for (let i = 0; i < particleCount; i++) { const t = Math.random(); // v7.90: Calculate particle position inline without clone() calls const particlePosX = startPos.x + direction.x * t * distance + (Math.random() - 0.5) * 6; const particlePosY = startPos.y + direction.y * t * distance + Math.random() * 2; const particlePosZ = startPos.z + direction.z * t * distance + (Math.random() - 0.5) * 6; // v7.90: Use pooled heatParticle geometry const particleMat = new THREE.MeshBasicMaterial({ color: Math.random() > 0.5 ? 0xffff88 : 0xffffff, transparent: true, opacity: 0.6 }); const particle = new THREE.Mesh(_effectGeometryPool.heatParticle, particleMat); particle.position.set(particlePosX, particlePosY, particlePosZ); particle.userData = { createdAt: performance.now(), lifetime: 2000, type: 'heatParticle', usesPooledGeo: true, // v7.90: Flag to skip dispose on pooled geometry // v7.90: Store velocity as plain object to avoid Vector3 allocation velocity: { x: (Math.random() - 0.5) * 2, y: 3 + Math.random() * 4, // Rising heat z: (Math.random() - 0.5) * 2 } }; scene.add(particle); fogClearEffects.push(particle); } // Show notification // v7.90: Use GlobalVec3Pool.temp() for floater position const floaterPos = GlobalVec3Pool.temp(); floaterPos.set(startPos.x, startPos.y + 4, startPos.z); spawnFloater(floaterPos, '🔥 FOG CLEARED! 🔥', '#ffff00'); showNotification('Thermal shockwave dispersed the fog!', 'success'); // Schedule fog return setTimeout(() => { if (scene.fog && currentWeather === 'fog') { // Gradually restore fog const restoreStart = performance.now(); const restoreInterval = setInterval(() => { const elapsed = performance.now() - restoreStart; const progress = Math.min(1, elapsed / FOG_CLEAR_FADE_TIME); if (scene.fog) { scene.fog.near = 60 - progress * (60 - originalNear); scene.fog.far = 200 - progress * (200 - originalFar); } if (progress >= 1) { clearInterval(restoreInterval); fogClearActive = false; } }, 50); } else { fogClearActive = false; } }, FOG_CLEAR_DURATION); } // Update fog clear visual effects function updateFogClearEffects(dt) { const now = performance.now(); const toRemove = []; for (const effect of fogClearEffects) { const age = now - effect.userData.createdAt; const progress = age / effect.userData.lifetime; if (progress >= 1) { toRemove.push(effect); continue; } if (effect.userData.type === 'fogRing') { // Expanding horizontal rings const scale = effect.userData.initialScale + effect.userData.expandRate * progress; effect.scale.setScalar(scale); effect.material.opacity = 0.5 * (1 - progress); } else if (effect.userData.type === 'heatParticle') { // Rising heat particles const vel = effect.userData.velocity; effect.position.x += vel.x * dt; effect.position.y += vel.y * dt; effect.position.z += vel.z * dt; vel.y -= 2 * dt; // Slight deceleration effect.material.opacity = 0.6 * (1 - progress); effect.scale.setScalar(1 - progress * 0.5); } } // Cleanup // v7.90: Don't dispose pooled geometry (marked with usesPooledGeo flag) for (const effect of toRemove) { scene.remove(effect); if (!effect.userData?.usesPooledGeo) { effect.geometry?.dispose(); } effect.material?.dispose(); } fogClearEffects = fogClearEffects.filter(e => !toRemove.includes(e)); } // v4.1: Create nebula clouds for galaxy atmosphere function createNebulae() { const nebulaColors = [0xff3366, 0x3366ff, 0x66ff33, 0xff6633, 0x9933ff, 0x33ffff]; const nebulaCount = 6; for (let i = 0; i < nebulaCount; i++) { const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256; const ctx = canvas.getContext('2d'); // Create procedural nebula with radial gradients const color = nebulaColors[i % nebulaColors.length]; const r = (color >> 16) & 255; const g = (color >> 8) & 255; const b = color & 255; // Multiple overlapping gradients for organic look for (let j = 0; j < 3; j++) { const cx = 80 + Math.random() * 96; const cy = 80 + Math.random() * 96; const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, 100 + Math.random() * 56); gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.25)`); gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.1)`); gradient.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, 256, 256); } const texture = new THREE.CanvasTexture(canvas); const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, depthWrite: false, opacity: 0.6 }); const geometry = new THREE.PlaneGeometry(600, 600); const nebula = new THREE.Mesh(geometry, material); // Position nebulae around the galaxy const angle = (i / nebulaCount) * Math.PI * 2; const dist = 400 + Math.random() * 600; nebula.position.set( Math.cos(angle) * dist, (Math.random() - 0.5) * 300, Math.sin(angle) * dist ); // Random rotation nebula.rotation.set( Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI ); scene.add(nebula); } } const BIOMES = { // v7.24: Fixed Terra water color from 0x2244aa to 0x2266cc (8-Strategy Consensus - more saturated blue) Terra: { sky: 0x87ceeb, ground: 0x33aa33, tree: 0x228b22, rock: 0x888888, water: 0x2266cc, name: 'Terra' }, Desert: { sky: 0xffcc99, ground: 0xeeddaa, tree: 0xccbb99, rock: 0xaa5522, water: 0x446688, name: 'Desert' }, Ice: { sky: 0xddeeff, ground: 0xffffff, tree: 0xaaccff, rock: 0x99aabb, water: 0x88aadd, name: 'Tundra' }, Alien: { sky: 0x220044, ground: 0x440066, tree: 0xff00ff, rock: 0x00ffcc, water: 0x8800ff, name: 'Xeno' }, Volcanic: { sky: 0x330000, ground: 0x221111, tree: 0x552222, rock: 0x111111, water: 0xff4400, name: 'Magma' }, // v7.24: ENHANCED FACTORY BIOME COLORS (8-Strategy Consensus) Factory: { sky: 0x2a3038, ground: 0x333344, tree: 0x666677, rock: 0x555566, water: 0x3a5570, name: 'Industrial', isFactory: true } }; // ================================================================ // MINECRAFT-STYLE PROCEDURAL TEXTURE GENERATOR // Based on Notch's original texture generation algorithm // Creates 16x16 pixel-art textures procedurally // ================================================================ const MinecraftTextures = (function() { const TEXTURE_SIZE = 16; const textureCache = new Map(); // Simple seeded random for deterministic textures function seededRandom(seed) { let s = seed; return function() { s = (s * 9301 + 49297) % 233280; return s / 233280; }; } // Extract RGB components from hex color function hexToRgb(hex) { return { r: (hex >> 16) & 255, g: (hex >> 8) & 255, b: hex & 255 }; } // Clamp value between 0-255 function clamp(val) { return Math.max(0, Math.min(255, Math.floor(val))); } // Core Minecraft-style noise function function minecraftNoise(x, y, seed) { const rand = seededRandom(seed + x * 31 + y * 17); return rand(); } // Generate grass texture (Terra biome) function generateGrassTexture(baseColor, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rgb = hexToRgb(baseColor); const rand = seededRandom(seed); for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const idx = (y * TEXTURE_SIZE + x) * 4; // Minecraft grass: green base with noise variation let brightness = 0.7 + rand() * 0.3; // Add occasional darker spots (dirt showing through) if (rand() < 0.08) { brightness *= 0.5; } // Add occasional lighter spots (sun highlights) if (rand() < 0.05) { brightness *= 1.3; } // Vertical gradient for grass blade effect const gradient = 1 - (y / TEXTURE_SIZE) * 0.15; brightness *= gradient; data[idx] = clamp(rgb.r * brightness); data[idx + 1] = clamp(rgb.g * brightness); data[idx + 2] = clamp(rgb.b * brightness); data[idx + 3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } // Generate sand texture (Desert biome) function generateSandTexture(baseColor, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rgb = hexToRgb(baseColor); const rand = seededRandom(seed); for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const idx = (y * TEXTURE_SIZE + x) * 4; // Sand: grainy texture with color variation let brightness = 0.85 + rand() * 0.15; // Add occasional darker grains if (rand() < 0.15) { brightness *= 0.75; } // Slight color shift for some grains let rShift = 1, gShift = 1, bShift = 1; if (rand() < 0.1) { rShift = 1.05; gShift = 0.95; } data[idx] = clamp(rgb.r * brightness * rShift); data[idx + 1] = clamp(rgb.g * brightness * gShift); data[idx + 2] = clamp(rgb.b * brightness * bShift); data[idx + 3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } // Generate snow/ice texture (Ice biome) function generateSnowTexture(baseColor, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rgb = hexToRgb(baseColor); const rand = seededRandom(seed); for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const idx = (y * TEXTURE_SIZE + x) * 4; // Snow: mostly white with subtle blue shadows let brightness = 0.9 + rand() * 0.1; // Ice crystal effect - occasional sparkles if (rand() < 0.03) { brightness = 1.2; } // Subtle shadow areas if (rand() < 0.1) { brightness *= 0.85; } // Blue tint in shadows const blueShift = brightness < 0.9 ? 1.1 : 1; data[idx] = clamp(rgb.r * brightness); data[idx + 1] = clamp(rgb.g * brightness); data[idx + 2] = clamp(rgb.b * brightness * blueShift); data[idx + 3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } // Generate alien/xeno texture (Alien biome) function generateAlienTexture(baseColor, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rgb = hexToRgb(baseColor); const rand = seededRandom(seed); for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const idx = (y * TEXTURE_SIZE + x) * 4; // Alien: pulsating organic pattern const wave = Math.sin((x + y) * 0.5 + seed * 0.01) * 0.15; let brightness = 0.6 + rand() * 0.3 + wave; // Bioluminescent spots if (rand() < 0.05) { brightness = 1.5; } // Dark veins if ((x + y) % 4 === 0 && rand() < 0.3) { brightness *= 0.4; } // Color shift for organic feel const shift = rand() < 0.2 ? 1.2 : 1; data[idx] = clamp(rgb.r * brightness * shift); data[idx + 1] = clamp(rgb.g * brightness); data[idx + 2] = clamp(rgb.b * brightness * shift); data[idx + 3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } // Generate volcanic/magma texture (Volcanic biome) function generateVolcanicTexture(baseColor, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rgb = hexToRgb(baseColor); const rand = seededRandom(seed); for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const idx = (y * TEXTURE_SIZE + x) * 4; // Dark volcanic rock base let brightness = 0.4 + rand() * 0.4; let isLava = false; // Lava cracks/veins if (rand() < 0.08 || ((x * y) % 7 === 0 && rand() < 0.2)) { isLava = true; brightness = 1.0; } if (isLava) { // Glowing lava - orange/red data[idx] = clamp(255 * brightness); data[idx + 1] = clamp(100 * brightness * rand()); data[idx + 2] = clamp(20); } else { // Dark rock data[idx] = clamp(rgb.r * brightness); data[idx + 1] = clamp(rgb.g * brightness); data[idx + 2] = clamp(rgb.b * brightness); } data[idx + 3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } // Generate water texture function generateWaterTexture(baseColor, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rgb = hexToRgb(baseColor); const rand = seededRandom(seed); for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const idx = (y * TEXTURE_SIZE + x) * 4; // Water: wave pattern with caustics const wave1 = Math.sin((x + seed * 0.1) * 0.6) * 0.1; const wave2 = Math.sin((y + seed * 0.05) * 0.8) * 0.08; let brightness = 0.7 + wave1 + wave2 + rand() * 0.15; // Caustic highlights if (rand() < 0.04) { brightness = 1.3; } // Depth variation const depth = 1 - (y / TEXTURE_SIZE) * 0.2; brightness *= depth; data[idx] = clamp(rgb.r * brightness); data[idx + 1] = clamp(rgb.g * brightness); data[idx + 2] = clamp(rgb.b * brightness * 1.1); data[idx + 3] = 200; // Semi-transparent } } ctx.putImageData(imageData, 0, 0); return canvas; } // Generate stone/rock texture function generateStoneTexture(baseColor, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rgb = hexToRgb(baseColor); const rand = seededRandom(seed); for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const idx = (y * TEXTURE_SIZE + x) * 4; // Stone: rough with mineral veins let brightness = 0.5 + rand() * 0.4; // Dark cracks if (rand() < 0.1) { brightness *= 0.4; } // Mineral sparkles if (rand() < 0.03) { brightness = 1.2; } // Color variation for different minerals let rShift = 1, gShift = 1, bShift = 1; if (rand() < 0.08) { // Iron oxide tint rShift = 1.2; } else if (rand() < 0.05) { // Copper tint gShift = 1.15; } data[idx] = clamp(rgb.r * brightness * rShift); data[idx + 1] = clamp(rgb.g * brightness * gShift); data[idx + 2] = clamp(rgb.b * brightness * bShift); data[idx + 3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } // Generate wood/bark texture function generateWoodTexture(baseColor, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rgb = hexToRgb(baseColor); const rand = seededRandom(seed); for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const idx = (y * TEXTURE_SIZE + x) * 4; // Wood grain - vertical lines with variation const grain = Math.sin(x * 1.5 + rand() * 2) * 0.15; let brightness = 0.6 + grain + rand() * 0.25; // Bark ridges (horizontal bands) if (y % 4 < 1) { brightness *= 0.7; } // Knots if (rand() < 0.02) { brightness *= 0.5; } data[idx] = clamp(rgb.r * brightness); data[idx + 1] = clamp(rgb.g * brightness); data[idx + 2] = clamp(rgb.b * brightness); data[idx + 3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } // Generate leaf texture function generateLeafTexture(baseColor, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rgb = hexToRgb(baseColor); const rand = seededRandom(seed); for (let y = 0; y < TEXTURE_SIZE; y++) { for (let x = 0; x < TEXTURE_SIZE; x++) { const idx = (y * TEXTURE_SIZE + x) * 4; // Leaves: varied green with holes let brightness = 0.65 + rand() * 0.35; let alpha = 255; // Leaf veins (diagonal pattern) if ((x + y) % 5 === 0) { brightness *= 0.75; } // Light spots (sun through leaves) if (rand() < 0.08) { brightness = 1.2; } // Holes in leaves (for transparency) if (rand() < 0.12) { alpha = 0; } data[idx] = clamp(rgb.r * brightness); data[idx + 1] = clamp(rgb.g * brightness); data[idx + 2] = clamp(rgb.b * brightness); data[idx + 3] = alpha; } } ctx.putImageData(imageData, 0, 0); return canvas; } // Main texture creation function with caching function createTexture(type, baseColor, seed = 42) { const cacheKey = `${type}_${baseColor}_${seed}`; if (textureCache.has(cacheKey)) { return textureCache.get(cacheKey); } let canvas; switch (type) { case 'grass': canvas = generateGrassTexture(baseColor, seed); break; case 'sand': canvas = generateSandTexture(baseColor, seed); break; case 'snow': canvas = generateSnowTexture(baseColor, seed); break; case 'alien': canvas = generateAlienTexture(baseColor, seed); break; case 'volcanic': canvas = generateVolcanicTexture(baseColor, seed); break; case 'water': canvas = generateWaterTexture(baseColor, seed); break; case 'stone': canvas = generateStoneTexture(baseColor, seed); break; case 'wood': canvas = generateWoodTexture(baseColor, seed); break; case 'leaf': canvas = generateLeafTexture(baseColor, seed); break; default: canvas = generateGrassTexture(baseColor, seed); } const texture = new THREE.CanvasTexture(canvas); texture.magFilter = THREE.NearestFilter; // Pixelated look like Minecraft texture.minFilter = THREE.NearestFilter; texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(1, 1); textureCache.set(cacheKey, texture); return texture; } // Get the appropriate texture type for a biome function getBiomeGroundType(biomeName) { const typeMap = { 'Terra': 'grass', 'Desert': 'sand', 'Ice': 'snow', 'Alien': 'alien', 'Volcanic': 'volcanic' }; return typeMap[biomeName] || 'grass'; } // Create material for ground - clean look emphasizing terrain height/ridges // v6.81: Replaced noisy Minecraft textures with clean elevation-based shading function createGroundMaterial(biome, biomeName) { // Use MeshStandardMaterial for better lighting response on terrain return new THREE.MeshStandardMaterial({ color: biome.ground, roughness: 0.85, metalness: 0.05, flatShading: true // Emphasizes the blocky terrain ridges }); } // Create material for water/lava - biome-aware liquid material // v6.81: Replaced noisy texture with clean water material // v7.24: Fixed swamp-green appearance (8-Strategy Consensus) - increased opacity, added emissive blue // v10.0: FLOODED WATER FIX - Full opacity + strong emissive blue to eliminate green bleed-through // v10.1: LAVA FIX (8-Strategy Consensus) - Volcanic biome uses glowing orange-red lava material function createWaterMaterial(biome, biomeName) { // v10.1: 8-STRATEGY CONSENSUS - Check for Volcanic/Magma biome // All 8 agents agreed: function was ignoring biome parameters, causing brown lava if (biomeName === 'Volcanic') { return new THREE.MeshStandardMaterial({ color: 0xff4400, // v10.1: Bright orange-red lava (consensus: 6/8 agents) emissive: 0xff2200, // v10.1: Strong red-orange glow (consensus: 6/8 agents) emissiveIntensity: 1.0, // v10.1: High intensity for molten glow (consensus mode) roughness: 0.6, // v10.1: Molten rock texture (consensus median) metalness: 0.0, // v10.1: Lava is not metallic (consensus: 8/8 unanimous) transparent: false, flatShading: true }); } // Default: Blue water for all other biomes return new THREE.MeshStandardMaterial({ color: 0x2288dd, // v10.0: Bright blue water color emissive: 0x1166bb, // v10.0: Strong blue emissive for proper water appearance emissiveIntensity: 0.6, // v10.0: Higher intensity for vibrant blue roughness: 0.3, // v10.0: Moderate roughness for natural water metalness: 0.1, // v10.0: Low metalness - water isn't metallic transparent: false, // v10.0: FULLY OPAQUE - no green terrain bleed-through flatShading: true }); } // Create material for rocks - clean flat shaded // v6.81: Replaced noisy texture with clean material function createRockMaterial(biome) { return new THREE.MeshStandardMaterial({ color: biome.rock, roughness: 0.9, metalness: 0.1, flatShading: true }); } // Create material for tree trunks - clean flat shaded // v6.81: Replaced noisy texture with clean bark material function createWoodMaterial(color, seed = 999) { return new THREE.MeshStandardMaterial({ color: color, roughness: 0.8, metalness: 0.0, flatShading: true }); } // Create material for leaves - clean flat shaded // v6.81: Replaced noisy texture with clean foliage material function createLeafMaterial(color, seed = 555) { return new THREE.MeshStandardMaterial({ color: color, roughness: 0.7, metalness: 0.0, flatShading: true, side: THREE.DoubleSide }); } return { createTexture, createGroundMaterial, createWaterMaterial, createRockMaterial, createWoodMaterial, createLeafMaterial, getBiomeGroundType }; })(); // END MINECRAFT TEXTURE GENERATOR // ================================================================ // ================================================================ // v6.93: ORBITAL PLANET TEXTURE GENERATOR // Creates procedural textures for planets visible from galaxy view // ================================================================ const PlanetTextures = (function() { const TEXTURE_SIZE = 128; const textureCache = new Map(); function seededRandom(seed) { let s = seed; return function() { s = (s * 9301 + 49297) % 233280; return s / 233280; }; } function hexToRgb(hex) { return { r: (hex >> 16) & 255, g: (hex >> 8) & 255, b: hex & 255 }; } function clamp(val) { return Math.max(0, Math.min(255, Math.floor(val))); } function fbm(x, y, seed, octaves = 4) { let value = 0, amplitude = 0.5, frequency = 1; for (let i = 0; i < octaves; i++) { const nx = Math.floor(x * frequency * 100); const ny = Math.floor(y * frequency * 100); const r = seededRandom(nx * 31 + ny * 17 + seed + i * 100); value += amplitude * r(); amplitude *= 0.5; frequency *= 2; } return value; } function generateTerraPlanet(biome, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rand = seededRandom(seed); const groundRgb = hexToRgb(biome.ground), waterRgb = hexToRgb(biome.water), rockRgb = hexToRgb(biome.rock); for (let py = 0; py < TEXTURE_SIZE; py++) { for (let px = 0; px < TEXTURE_SIZE; px++) { const idx = (py * TEXTURE_SIZE + px) * 4; const nx = (px / TEXTURE_SIZE) * 4, ny = (py / TEXTURE_SIZE) * 4; const height = fbm(nx, ny, seed, 5); let r, g, b; if (height < 0.45) { r = waterRgb.r * (0.6 + (height/0.45) * 0.3); g = waterRgb.g * (0.6 + (height/0.45) * 0.3); b = waterRgb.b * (0.7 + (height/0.45) * 0.3); } else if (height < 0.5) { r = waterRgb.r * 0.7; g = waterRgb.g * 1.3; b = waterRgb.b * 1.3; } // v10.0: Shallow water = lighter cyan-blue, not greenish else if (height < 0.7) { const v = fbm(nx*2, ny*2, seed+50, 3) * 0.3; r = groundRgb.r * (0.8+v); g = groundRgb.g * (0.9+v); b = groundRgb.b * (0.7+v); } else if (height < 0.85) { r = groundRgb.r * 0.6; g = groundRgb.g * 0.7; b = groundRgb.b * 0.5; } else { if (rand() < (height-0.85)*5) { r=240; g=245; b=250; } else { r=rockRgb.r; g=rockRgb.g; b=rockRgb.b; } } data[idx] = clamp(r); data[idx+1] = clamp(g); data[idx+2] = clamp(b); data[idx+3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } function generateDesertPlanet(biome, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const groundRgb = hexToRgb(biome.ground), rockRgb = hexToRgb(biome.rock); for (let py = 0; py < TEXTURE_SIZE; py++) { for (let px = 0; px < TEXTURE_SIZE; px++) { const idx = (py * TEXTURE_SIZE + px) * 4; const nx = (px / TEXTURE_SIZE) * 4, ny = (py / TEXTURE_SIZE) * 4; const height = fbm(nx, ny, seed, 4); const dunes = Math.sin(nx * 3 + fbm(nx, ny, seed+100, 2) * 2) * 0.5 + 0.5; let r, g, b; if (height > 0.7) { r = rockRgb.r * (0.8+height*0.2); g = rockRgb.g * (0.8+height*0.2); b = rockRgb.b * (0.7+height*0.2); } else { const shade = 0.85 + dunes * 0.15; r = groundRgb.r * shade; g = groundRgb.g * shade * 0.95; b = groundRgb.b * shade * 0.9; } data[idx] = clamp(r); data[idx+1] = clamp(g); data[idx+2] = clamp(b); data[idx+3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } function generateIcePlanet(biome, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rand = seededRandom(seed); const groundRgb = hexToRgb(biome.ground), waterRgb = hexToRgb(biome.water); for (let py = 0; py < TEXTURE_SIZE; py++) { for (let px = 0; px < TEXTURE_SIZE; px++) { const idx = (py * TEXTURE_SIZE + px) * 4; const nx = (px / TEXTURE_SIZE) * 4, ny = (py / TEXTURE_SIZE) * 4; const height = fbm(nx, ny, seed, 4), cracks = fbm(nx*3, ny*3, seed+200, 2); let r, g, b; if (height < 0.4) { r = waterRgb.r * 0.7; g = waterRgb.g * 0.8; b = waterRgb.b * 0.9; } else if (cracks < 0.3) { r = 100; g = 140; b = 180; } else { const sp = rand() < 0.02 ? 1.1 : 1.0; r = groundRgb.r * 0.98 * sp; g = groundRgb.g * 0.98 * sp; b = Math.min(255, groundRgb.b * 1.05 * sp); } data[idx] = clamp(r); data[idx+1] = clamp(g); data[idx+2] = clamp(b); data[idx+3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } function generateAlienPlanet(biome, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const rand = seededRandom(seed); const groundRgb = hexToRgb(biome.ground), treeRgb = hexToRgb(biome.tree), waterRgb = hexToRgb(biome.water); for (let py = 0; py < TEXTURE_SIZE; py++) { for (let px = 0; px < TEXTURE_SIZE; px++) { const idx = (py * TEXTURE_SIZE + px) * 4; const nx = (px / TEXTURE_SIZE) * 4, ny = (py / TEXTURE_SIZE) * 4; const height = fbm(nx, ny, seed, 4); const organic = Math.sin(nx*5 + ny*3 + seed*0.1) * 0.5 + 0.5; let r, g, b; if (height < 0.35) { const glow = 0.8 + Math.sin(nx*10 + ny*8) * 0.2; r = waterRgb.r * glow; g = waterRgb.g * 0.5; b = waterRgb.b * glow; } else if (rand() < 0.08) { r = treeRgb.r; g = treeRgb.g * 0.3; b = treeRgb.b; } else { r = groundRgb.r * (0.7+organic*0.4); g = groundRgb.g * (0.5+organic*0.3); b = groundRgb.b * (0.8+organic*0.3); } data[idx] = clamp(r); data[idx+1] = clamp(g); data[idx+2] = clamp(b); data[idx+3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } function generateVolcanicPlanet(biome, seed) { const canvas = document.createElement('canvas'); canvas.width = canvas.height = TEXTURE_SIZE; const ctx = canvas.getContext('2d'); const imageData = ctx.createImageData(TEXTURE_SIZE, TEXTURE_SIZE); const data = imageData.data; const groundRgb = hexToRgb(biome.ground), lavaRgb = hexToRgb(biome.water); for (let py = 0; py < TEXTURE_SIZE; py++) { for (let px = 0; px < TEXTURE_SIZE; px++) { const idx = (py * TEXTURE_SIZE + px) * 4; const nx = (px / TEXTURE_SIZE) * 4, ny = (py / TEXTURE_SIZE) * 4; const height = fbm(nx, ny, seed, 5), lavaFlow = fbm(nx*2, ny*2, seed+300, 3); let r, g, b; if (lavaFlow < 0.25 || height < 0.3) { const heat = 0.8 + Math.sin(nx*8 + ny*6 + seed*0.05) * 0.2; r = lavaRgb.r * heat; g = lavaRgb.g * heat * 0.5; b = 0; } else { const rv = 0.6 + height * 0.4; r = groundRgb.r * rv; g = groundRgb.g * rv; b = groundRgb.b * rv; } data[idx] = clamp(r); data[idx+1] = clamp(g); data[idx+2] = clamp(b); data[idx+3] = 255; } } ctx.putImageData(imageData, 0, 0); return canvas; } function createPlanetMaterial(biomeKey, planetSeed) { const cacheKey = `${biomeKey}-${planetSeed}`; if (textureCache.has(cacheKey)) return textureCache.get(cacheKey).clone(); const biome = BIOMES[biomeKey] || BIOMES.Terra; let canvas; switch (biomeKey) { case 'Desert': canvas = generateDesertPlanet(biome, planetSeed); break; case 'Ice': canvas = generateIcePlanet(biome, planetSeed); break; case 'Alien': canvas = generateAlienPlanet(biome, planetSeed); break; case 'Volcanic': canvas = generateVolcanicPlanet(biome, planetSeed); break; default: canvas = generateTerraPlanet(biome, planetSeed); } const texture = new THREE.CanvasTexture(canvas); texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; const material = new THREE.MeshBasicMaterial({ map: texture }); textureCache.set(cacheKey, material); return material; } function createAtmosphereMaterial(biomeKey) { const biome = BIOMES[biomeKey] || BIOMES.Terra; return new THREE.MeshBasicMaterial({ color: biome.sky, transparent: true, opacity: 0.15, side: THREE.BackSide }); } return { createPlanetMaterial, createAtmosphereMaterial }; })(); // END ORBITAL PLANET TEXTURE GENERATOR // ================================================================ // ============================================ // v8.0: ATTACK INTERRUPT/STAGGER SYSTEM - 8-Agent Consensus (Cycle 5) // Deal enough damage during enemy telegraph to cancel their attack! // ============================================ const STAGGER_CONFIG = { // Base stagger thresholds per enemy type (set in ENEMY_TYPES) DEFAULT_THRESHOLD: 15, // Default damage needed to stagger ELITE_MULTIPLIER: 2.0, // Elites need 2x damage to stagger BOSS_MULTIPLIER: 3.0, // Bosses need 3x damage // Stagger effects STAGGER_DURATION: 600, // How long enemy is stunned after stagger STAGGER_COOLDOWN: 3000, // Enemies can't be staggered again for 3s // Audio/Visual feedback STAGGER_PARTICLES: 15, STAGGER_COLOR: 0xffff00, // Yellow flash on stagger // Bonus rewards COMBO_MULTIPLIER_BONUS: 0.1, // +10% combo bonus for staggers STYLE_BONUS: 50 // Style meter points for stagger }; // Track damage dealt during telegraph for each mob const staggerTracking = new Map(); function initStaggerTracking(mobId) { staggerTracking.set(mobId, { damageAccumulated: 0, lastStaggerTime: 0 }); } function accumulateStaggerDamage(mob, damage) { if (!mob || !mob.userData) return false; const mobId = mob.uuid || mob.id; if (!staggerTracking.has(mobId)) { initStaggerTracking(mobId); } const tracking = staggerTracking.get(mobId); const now = performance.now(); // Check if mob is on stagger cooldown if (now - tracking.lastStaggerTime < STAGGER_CONFIG.STAGGER_COOLDOWN) { return false; } // Only accumulate if mob is telegraphing if (!mob.userData.telegraphing) { tracking.damageAccumulated = 0; return false; } // Accumulate damage tracking.damageAccumulated += damage; // Calculate threshold const enemyType = ENEMY_TYPES[mob.userData.name]; let threshold = enemyType?.staggerThreshold || STAGGER_CONFIG.DEFAULT_THRESHOLD; // Apply multipliers for elite/boss if (mob.userData.isElite) { threshold *= STAGGER_CONFIG.ELITE_MULTIPLIER; } if (mob.userData.isBoss || mob.userData.type === 'boss') { threshold *= STAGGER_CONFIG.BOSS_MULTIPLIER; } // Check if stagger threshold reached if (tracking.damageAccumulated >= threshold) { triggerStagger(mob); tracking.damageAccumulated = 0; tracking.lastStaggerTime = now; return true; } return false; } function triggerStagger(mob) { if (!mob || !mob.userData) return; // Cancel the telegraph mob.userData.telegraphing = false; mob.userData.stunned = true; mob.userData.stunnedUntil = performance.now() + STAGGER_CONFIG.STAGGER_DURATION; // Reset attack timing (give player a window) mob.userData.nextAttack = performance.now() + STAGGER_CONFIG.STAGGER_DURATION + 500; // Restore original emissive // v10.12: Added emissive property checks if (mob.material?.emissive && mob.userData.originalEmissive !== undefined) { mob.material.emissive.setHex(mob.userData.originalEmissive); } // Visual feedback - yellow stagger flash if (mob.material?.emissive) { const origEmissive = mob.material.emissive.getHex(); mob.material.emissive.setHex(STAGGER_CONFIG.STAGGER_COLOR); setTimeout(() => { if (mob.material?.emissive) mob.material.emissive.setHex(origEmissive); }, 200); } // Scale recoil animation if (mob.scale) { mob.scale.setScalar(0.7); setTimeout(() => { if (mob.scale) mob.scale.setScalar(1); }, 150); } // Particles if (particles && mob.position) { particles.emit(mob.position, STAGGER_CONFIG.STAGGER_PARTICLES, STAGGER_CONFIG.STAGGER_COLOR, { spread: 3, lifetime: 400 }); } // Audio feedback playStaggerSound(); // Floater notification - v7.91: Use pooled position if (mob.position) { spawnFloater(getFloaterPos(mob.position, 1.5), '💥 STAGGERED!', '#ffff00'); } // Style bonus if (typeof updateStyleMeter === 'function') { updateStyleMeter('parry', 2); // Use parry action for bonus, x2 multiplier } // Track for behavioral pattern if (typeof trackBehaviorPattern === 'function') { trackBehaviorPattern('attack'); } } function playStaggerSound() { // v7.28: Use shared AudioContext const audioCtx = getSharedAudioContext(); if (!audioCtx) return; try { const masterGain = audioCtx.createGain(); masterGain.gain.value = 0.25; masterGain.connect(audioCtx.destination); // Impact thump const osc1 = audioCtx.createOscillator(); const gain1 = audioCtx.createGain(); osc1.type = 'sine'; osc1.frequency.value = 120; osc1.frequency.exponentialRampToValueAtTime(40, audioCtx.currentTime + 0.15); gain1.gain.setValueAtTime(0.8, audioCtx.currentTime); gain1.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2); osc1.connect(gain1); gain1.connect(masterGain); osc1.start(); osc1.stop(audioCtx.currentTime + 0.2); // High crack const osc2 = audioCtx.createOscillator(); const gain2 = audioCtx.createGain(); osc2.type = 'square'; osc2.frequency.value = 800; gain2.gain.setValueAtTime(0.3, audioCtx.currentTime); gain2.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.08); osc2.connect(gain2); gain2.connect(masterGain); osc2.start(); osc2.stop(audioCtx.currentTime + 0.08); } catch (e) { console.log('Stagger sound error:', e); } } function cleanupStaggerTracking(mobId) { staggerTracking.delete(mobId); } // ============================================ // v9.0: CREATURE MODEL LOADER SYSTEM // Loads detailed 3D creature models from JSON // ============================================ const CREATURE_MODEL_CONFIG = { ENABLED: true, BASE_URL: 'https://raw.githubusercontent.com/kody-w/localFirstTools/main/data/games/creatures/', CACHE: new Map(), LOADING: new Map(), FALLBACK_TO_SPHERE: true }; // Maps enemy names to creature model files const CREATURE_MODEL_REGISTRY = { // Regular enemies 'ShadowWraith': 'shadow-wraith.json', 'CrystalGolem': 'crystal-golem.json', 'VoidSpawn': 'void-spider.json', 'MagmaCore': 'flame-serpent.json', 'IceWisp': 'storm-elemental.json', 'Berserker': 'blood-knight.json', 'Slime': 'forest-guardian.json', // Neutral creep camp creatures 'CaveBat': 'cave-bat.json', 'ForestSprite': 'forest-sprite.json', 'RockTroll': 'rock-troll.json', 'WolfAlpha': 'wolf-alpha.json', 'AncientWyrm': 'ancient-wyrm.json', 'TitanGolem': 'titan-golem.json' }; // Fetch and cache creature model JSON async function loadCreatureModel(modelFile) { if (CREATURE_MODEL_CONFIG.CACHE.has(modelFile)) { return CREATURE_MODEL_CONFIG.CACHE.get(modelFile); } if (CREATURE_MODEL_CONFIG.LOADING.has(modelFile)) { return CREATURE_MODEL_CONFIG.LOADING.get(modelFile); } const loadPromise = fetch(CREATURE_MODEL_CONFIG.BASE_URL + modelFile) .then(r => { if (!r.ok) throw new Error('Model fetch failed'); return r.json(); }) .then(data => { CREATURE_MODEL_CONFIG.CACHE.set(modelFile, data); CREATURE_MODEL_CONFIG.LOADING.delete(modelFile); return data; }) .catch(err => { console.warn(`Failed to load creature model ${modelFile}:`, err); CREATURE_MODEL_CONFIG.LOADING.delete(modelFile); return null; }); CREATURE_MODEL_CONFIG.LOADING.set(modelFile, loadPromise); return loadPromise; } // Pre-load all creature models at game start function preloadCreatureModels() { Object.values(CREATURE_MODEL_REGISTRY).forEach(modelFile => { loadCreatureModel(modelFile); }); } // Create 3D mesh from creature JSON bodyParts function createCreatureMeshFromJSON(creatureData, scale = 0.5) { const group = new THREE.Group(); if (!creatureData || !creatureData.bodyParts) return null; creatureData.bodyParts.forEach(part => { let geometry; const size = (part.size || 0.5) * scale; switch (part.shape) { case 'sphere': geometry = new THREE.SphereGeometry(size, 16, 16); break; case 'box': const w = (part.width || size) * scale; const h = (part.height || size) * scale; const d = (part.depth || size) * scale; geometry = new THREE.BoxGeometry(w, h, d); break; case 'cylinder': const rTop = (part.radiusTop || size * 0.5) * scale; const rBot = (part.radiusBottom || size * 0.5) * scale; const height = (part.height || size) * scale; geometry = new THREE.CylinderGeometry(rTop, rBot, height, 16); break; case 'cone': const cHeight = (part.height || size) * scale; geometry = new THREE.ConeGeometry(size, cHeight, 16); break; case 'torus': const tubeR = (part.tubeRadius || size * 0.2) * scale; geometry = new THREE.TorusGeometry(size, tubeR, 12, 24); break; default: geometry = new THREE.SphereGeometry(size, 16, 16); } const color = new THREE.Color(part.color || '#ffffff'); const material = new THREE.MeshStandardMaterial({ color: color, roughness: 1.0 - (part.shininess || 50) / 100, metalness: (part.shininess || 50) / 200, transparent: part.opacity < 1, opacity: part.opacity || 1, emissive: part.glowing ? color : new THREE.Color(0x000000), emissiveIntensity: part.glowing ? 0.5 : 0 }); const mesh = new THREE.Mesh(geometry, material); // Apply position (scaled) if (part.position) { mesh.position.set( part.position[0] * scale, part.position[1] * scale, part.position[2] * scale ); } // Apply rotation if (part.rotation) { mesh.rotation.set(part.rotation[0], part.rotation[1], part.rotation[2]); } // Apply scale if (part.scale) { mesh.scale.set(part.scale[0], part.scale[1], part.scale[2]); } mesh.castShadow = true; mesh.receiveShadow = true; group.add(mesh); }); return group; } // v8.23: Pre-allocated Color for elite mob tinting to avoid allocations during spawn let _eliteTintColor = null; function getEliteTintColor() { if (!_eliteTintColor) _eliteTintColor = new THREE.Color(); return _eliteTintColor; } // Create mob mesh - uses creature model if available, falls back to sphere async function createMobMeshAsync(enemyName, enemyData, isElite, eliteData) { const modelFile = CREATURE_MODEL_REGISTRY[enemyName]; if (CREATURE_MODEL_CONFIG.ENABLED && modelFile) { const creatureData = await loadCreatureModel(modelFile); if (creatureData) { const creatureMesh = createCreatureMeshFromJSON(creatureData, isElite ? 0.6 : 0.4); if (creatureMesh) { // Tint with elite color if elite // v8.23: Use pre-allocated Color and copy to avoid allocations if (isElite && eliteData) { const eliteColor = getEliteTintColor().set(eliteData.color); creatureMesh.traverse(child => { if (child.isMesh && child.material) { child.material = child.material.clone(); child.material.emissive.copy(eliteColor); child.material.emissiveIntensity += 0.3; } }); } return creatureMesh; } } } // Fallback to simple sphere const mobGeo = new THREE.SphereGeometry(isElite ? 1.0 : 0.8, 16, 16); const mobMat = new THREE.MeshStandardMaterial({ color: eliteData ? eliteData.color : enemyData.color, roughness: 0.3, emissive: eliteData ? eliteData.color : enemyData.emissive, emissiveIntensity: isElite ? 0.5 : 0.2 }); return new THREE.Mesh(mobGeo, mobMat); } // Sync version - uses cached model or falls back immediately function createMobMeshSync(enemyName, enemyData, isElite, eliteData) { const modelFile = CREATURE_MODEL_REGISTRY[enemyName]; if (CREATURE_MODEL_CONFIG.ENABLED && modelFile) { const cachedData = CREATURE_MODEL_CONFIG.CACHE.get(modelFile); if (cachedData) { const creatureMesh = createCreatureMeshFromJSON(cachedData, isElite ? 0.6 : 0.4); if (creatureMesh) { // v8.23: Use pre-allocated Color and copy to avoid allocations if (isElite && eliteData) { const eliteColor = getEliteTintColor().set(eliteData.color); creatureMesh.traverse(child => { if (child.isMesh && child.material) { child.material = child.material.clone(); child.material.emissive.copy(eliteColor); child.material.emissiveIntensity += 0.3; } }); } return creatureMesh; } } } // Fallback to simple sphere const mobGeo = new THREE.SphereGeometry(isElite ? 1.0 : 0.8, 16, 16); const mobMat = new THREE.MeshStandardMaterial({ color: eliteData ? eliteData.color : enemyData.color, roughness: 0.3, emissive: eliteData ? eliteData.color : enemyData.emissive, emissiveIntensity: isElite ? 0.5 : 0.2 }); return new THREE.Mesh(mobGeo, mobMat); } // v4.2: Enemy Variety System - Biome-specific enemies // v4.5: Added attack telegraphing parameters const ENEMY_TYPES = { Slime: { hp: 10, damage: 5, speed: 4, color: 0x44ff44, emissive: 0x003300, drops: ['Slime'], xp: 100, biomes: ['Terra', 'Alien'], attackWindup: 800, attackRange: 2.5, // v4.5: Telegraph timing staggerThreshold: 8 // v8.0: Low threshold - easy to stagger }, Scorpion: { hp: 15, damage: 8, speed: 5, color: 0xdd9944, emissive: 0x442200, drops: ['Chitin'], xp: 150, biomes: ['Desert'], attackWindup: 600, attackRange: 3.0, staggerThreshold: 12 // v8.0: Medium threshold }, IceWisp: { hp: 8, damage: 6, speed: 7, color: 0x88ccff, emissive: 0x002244, drops: ['Frost Shard'], xp: 120, biomes: ['Ice'], attackWindup: 500, attackRange: 4.0, // Fast ranged staggerThreshold: 6 // v8.0: Fragile but fast }, MagmaCore: { hp: 20, damage: 10, speed: 3, color: 0xff4400, emissive: 0x440000, drops: ['Magma Gem'], xp: 180, biomes: ['Volcanic'], attackWindup: 1200, attackRange: 3.5, // Slow heavy staggerThreshold: 25 // v8.0: Tough - hard to stagger }, VoidSpawn: { hp: 25, damage: 12, speed: 5, color: 0x8800ff, emissive: 0x220044, drops: ['Void Fragment'], xp: 250, biomes: ['Alien'], attackWindup: 700, attackRange: 3.0, staggerThreshold: 20 // v8.0: Resistant }, // v5.12: Hypnotist - Special enemy that takes control of the player Hypnotist: { hp: 35, damage: 8, speed: 2, color: 0xff00ff, emissive: 0x660066, drops: ['Void Fragment', 'Psychic Shard'], xp: 400, biomes: ['Alien', 'Volcanic'], attackWindup: 2000, attackRange: 15.0, // Long range hypnosis isHypnotist: true, hypnosisRange: 12, hypnosisDuration: 8000, hypnosisCooldown: 15000 }, // v6.1: NEW ENEMY TYPES Mimic: { hp: 40, damage: 15, speed: 8, color: 0xcd853f, emissive: 0x442200, drops: ['Mimic Tooth', 'Treasure Key'], xp: 350, biomes: ['Terra', 'Desert', 'Alien'], attackWindup: 300, attackRange: 3.5, isMimic: true, // Disguises as resource node disguiseType: 'chest', // chest, rock, tree revealRange: 5, // Distance to reveal disguise ambushDamage: 25 // Extra damage on first hit }, Summoner: { hp: 25, damage: 5, speed: 2, color: 0x9932cc, emissive: 0x330066, drops: ['Summoner Staff', 'Soul Essence'], xp: 300, biomes: ['Alien', 'Ice'], attackWindup: 1500, attackRange: 20.0, isSummoner: true, summonCooldown: 8000, summonCount: 2, // Spawns 2 minions at a time maxMinions: 4, // Maximum minions alive at once minions: [] // Track active minions }, ShadowWraith: { hp: 18, damage: 12, speed: 9, color: 0x111111, emissive: 0x220022, drops: ['Shadow Essence', 'Dark Crystal'], xp: 280, biomes: ['Alien'], attackWindup: 400, attackRange: 3.0, isShadow: true, phaseChance: 0.3, // 30% chance to phase through attacks onlyDuringEclipse: true // Only spawns during Solar Eclipse event }, CrystalGolem: { hp: 60, damage: 18, speed: 2, color: 0x00ffff, emissive: 0x004444, drops: ['Crystal', 'Golem Core', 'Rare Crystal'], xp: 400, biomes: ['Ice', 'Terra'], attackWindup: 1800, attackRange: 4.0, hasShield: true, shieldHp: 30, // Shield absorbs first 30 damage shieldRegen: 5000 // Shield regenerates after 5 seconds }, Berserker: { hp: 30, damage: 10, speed: 5, color: 0xff2200, emissive: 0x440000, drops: ['Berserker Blood', 'Rage Shard'], xp: 320, biomes: ['Volcanic', 'Desert'], attackWindup: 600, attackRange: 3.0, isBerserker: true, rageThreshold: 0.3, // Enters rage at 30% HP rageDamageBonus: 1.5, // +50% damage when enraged rageSpeedBonus: 1.4 // +40% speed when enraged } }; // v4.6: Elemental Status Effects System const STATUS_EFFECTS = { ice: { name: 'Frozen', duration: 3000, color: 0x88ccff, icon: '❄️', speedMod: 0.3 // Slows to 30% speed }, fire: { name: 'Burning', duration: 4000, color: 0xff4400, icon: '🔥', tickRate: 500, tickDamage: 2 }, void: { name: 'Weakened', duration: 5000, color: 0x8800ff, icon: '💜', damageMod: 0.5 // Enemy deals 50% damage }, cosmic: { name: 'Annihilated', duration: 3000, color: 0xffd700, icon: '✨', tickRate: 250, tickDamage: 5, speedMod: 0.5 } }; // ============================================ // v8.0: ELEMENTAL CHAIN REACTIONS - 8-Agent Consensus Cycle 7 // Status effects interact when enemies are near each other! // ============================================ const ELEMENTAL_CHAIN_CONFIG = { ENABLED: true, PROXIMITY_RANGE: 3.5, CHECK_INTERVAL: 500, REACTIONS: { 'fire+fire': { type: 'spread', element: 'fire', durationBonus: 0.5, message: '🔥 FIRE SPREADS!', color: 0xff6600 }, 'fire+ice': { type: 'explosion', damage: 12, clearsBoth: true, message: '💨 STEAM EXPLOSION!', color: 0xaaddff }, 'void+void': { type: 'amplify', damageModBonus: 0.25, message: '💜 VOID AMPLIFIED!', color: 0xaa00ff }, 'cosmic+any': { type: 'dominate', element: 'cosmic', message: '✨ COSMIC CASCADE!', color: 0xffd700 } }, STYLE_BONUS: 40 }; let chainReactionState = { lastCheckTime: 0, recentReactions: new Set() }; // v7.96: Spatial hash grid for O(n) elemental chain detection instead of O(n^2) const ElementalSpatialGrid = { cellSize: 4, // Slightly larger than PROXIMITY_RANGE (3.5) to ensure neighbors are found grid: new Map(), // Clear and rebuild grid each frame clear() { this.grid.clear(); }, // Get cell key from position getCellKey(x, z) { const cx = Math.floor(x / this.cellSize); const cz = Math.floor(z / this.cellSize); return `${cx},${cz}`; }, // Insert mob into grid insert(mob) { const key = this.getCellKey(mob.position.x, mob.position.z); if (!this.grid.has(key)) { this.grid.set(key, []); } this.grid.get(key).push(mob); }, // Get all mobs in cell and adjacent cells getNeighbors(mob) { const cx = Math.floor(mob.position.x / this.cellSize); const cz = Math.floor(mob.position.z / this.cellSize); const neighbors = []; // Check 3x3 grid of cells around mob for (let dx = -1; dx <= 1; dx++) { for (let dz = -1; dz <= 1; dz++) { const key = `${cx + dx},${cz + dz}`; const cell = this.grid.get(key); if (cell) { for (let i = 0; i < cell.length; i++) { if (cell[i] !== mob) neighbors.push(cell[i]); } } } } return neighbors; } }; function checkElementalChainReactions() { if (!ELEMENTAL_CHAIN_CONFIG.ENABLED || !worldState?.mobs?.length) return; const now = performance.now(); if (now - chainReactionState.lastCheckTime < ELEMENTAL_CHAIN_CONFIG.CHECK_INTERVAL) return; chainReactionState.lastCheckTime = now; chainReactionState.recentReactions.clear(); const mobs = worldState.mobs; const proximityRangeSq = ELEMENTAL_CHAIN_CONFIG.PROXIMITY_RANGE * ELEMENTAL_CHAIN_CONFIG.PROXIMITY_RANGE; // v7.96: Use spatial grid for O(n) neighbor detection // Only use grid if we have enough mobs to benefit (overhead not worth it for small counts) if (mobs.length > 20) { ElementalSpatialGrid.clear(); // Build grid with only mobs that have status effects const affectedMobs = []; for (let i = 0; i < mobs.length; i++) { const mob = mobs[i]; if (mob?.userData?.statusEffects && Object.keys(mob.userData.statusEffects).length > 0) { ElementalSpatialGrid.insert(mob); affectedMobs.push(mob); } } // Check each affected mob against its spatial neighbors const checked = new Set(); for (let i = 0; i < affectedMobs.length; i++) { const mobA = affectedMobs[i]; const neighbors = ElementalSpatialGrid.getNeighbors(mobA); for (let j = 0; j < neighbors.length; j++) { const mobB = neighbors[j]; // Avoid checking pairs twice const pairId = mobA.id < mobB.id ? `${mobA.id}-${mobB.id}` : `${mobB.id}-${mobA.id}`; if (checked.has(pairId)) continue; checked.add(pairId); const distSq = mobA.position.distanceToSquared(mobB.position); if (distSq > proximityRangeSq) continue; for (const elemA of Object.keys(mobA.userData.statusEffects)) { for (const elemB of Object.keys(mobB.userData.statusEffects)) { triggerElementalReaction(mobA, mobB, elemA, elemB); } } } } } else { // v7.72: Original O(n^2) loop for small mob counts (overhead of grid not worth it) for (let i = 0; i < mobs.length; i++) { const mobA = mobs[i]; if (!mobA?.userData?.statusEffects) continue; for (let j = i + 1; j < mobs.length; j++) { const mobB = mobs[j]; if (!mobB?.userData?.statusEffects) continue; const distSq = mobA.position.distanceToSquared(mobB.position); if (distSq > proximityRangeSq) continue; for (const elemA of Object.keys(mobA.userData.statusEffects)) { for (const elemB of Object.keys(mobB.userData.statusEffects)) { triggerElementalReaction(mobA, mobB, elemA, elemB); } } } } } } function triggerElementalReaction(mobA, mobB, elemA, elemB) { const reactionKey = [elemA, elemB].sort().join('+'); let reaction = ELEMENTAL_CHAIN_CONFIG.REACTIONS[reactionKey]; if (!reaction && (elemA === 'cosmic' || elemB === 'cosmic')) { reaction = ELEMENTAL_CHAIN_CONFIG.REACTIONS['cosmic+any']; } if (!reaction) return; const pairKey = [mobA.uuid, mobB.uuid].sort().join('-') + reactionKey; if (chainReactionState.recentReactions.has(pairKey)) return; chainReactionState.recentReactions.add(pairKey); // v7.95: Use temp vector to avoid clone() allocation const midpoint = GlobalVec3Pool.temp().copy(mobA.position).lerp(mobB.position, 0.5); switch (reaction.type) { case 'spread': const effA = mobA.userData.statusEffects[reaction.element]; const effB = mobB.userData.statusEffects[reaction.element]; if (effA) effA.endTime += STATUS_EFFECTS[reaction.element].duration * reaction.durationBonus; if (effB) effB.endTime += STATUS_EFFECTS[reaction.element].duration * reaction.durationBonus; break; case 'explosion': mobA.userData.hp -= reaction.damage; mobB.userData.hp -= reaction.damage; if (reaction.clearsBoth) { delete mobA.userData.statusEffects[elemA]; delete mobA.userData.statusEffects[elemB]; delete mobB.userData.statusEffects[elemA]; delete mobB.userData.statusEffects[elemB]; } if (typeof screenShake === 'function') screenShake(0.3); break; case 'amplify': mobA.userData.damageMultiplier = (mobA.userData.damageMultiplier || 1) - reaction.damageModBonus; mobB.userData.damageMultiplier = (mobB.userData.damageMultiplier || 1) - reaction.damageModBonus; break; case 'dominate': const targetMob = elemA === 'cosmic' ? mobB : mobA; if (!targetMob.userData.statusEffects['cosmic']) { applyStatusEffect(targetMob, 'cosmic'); } break; } spawnFloater(midpoint, reaction.message, '#' + reaction.color.toString(16).padStart(6, '0')); if (particles) particles.emit(midpoint, 15, reaction.color, { spread: 4, lifetime: 600, size: 0.2 }); AudioSystem.hit(); if (typeof updateStyleMeter === 'function') updateStyleMeter('kill', ELEMENTAL_CHAIN_CONFIG.STYLE_BONUS / 100); } // ============================================ // v5.12: HYPNOTIST SYSTEM // Eye animation, trance effects, and break-free mechanics // ============================================ const HYPNOSIS_STATE = { active: false, hypnotistMob: null, startTime: 0, duration: 0, spiralAngle: 0, eyePhase: 0, breakAttempts: 0, maxBreakAttempts: 3, breakDamage: 25, tranceOverlay: null, spiralRings: [], companionEyeOffset: { x: 0, y: 0 }, // v6.13: Enhanced mind control effects mindControlEffect: null, // Current active effect controlsInverted: false, // Inverted movement lastMindControlAction: 0, // Timestamp of last involuntary action resourcesDrained: 0, // Track resources lost during hypnosis attackedAllies: 0 // Track friendly fire incidents }; // v6.13: Mind Control Effect Types - Robot performs actions against its mission const MIND_CONTROL_EFFECTS = { invertControls: { name: 'Inverted Controls', icon: '🔄', message: 'Your controls are reversed!', color: '#ff00ff' }, resourceDrain: { name: 'Resource Leak', icon: '💸', message: 'You are dropping resources!', color: '#ffaa00' }, friendlyFire: { name: 'Confused Targeting', icon: '🎯', message: 'You might attack allies!', color: '#ff4444' }, selfSabotage: { name: 'Self Sabotage', icon: '⚠️', message: 'Your systems are compromised!', color: '#ff6600' } }; // Autopilot mode flag (disables player control during certain states) let autopilotEnabled = false; // Eye animation patterns for hypnosis const HYPNO_EYE_PATTERNS = [ { name: 'spiral', fn: (t) => ({ x: Math.cos(t * 3) * 0.3, y: Math.sin(t * 3) * 0.15 }) }, { name: 'figure8', fn: (t) => ({ x: Math.sin(t * 2) * 0.4, y: Math.sin(t * 4) * 0.2 }) }, { name: 'pendulum', fn: (t) => ({ x: Math.sin(t * 2.5) * 0.5, y: 0 }) }, { name: 'erratic', fn: (t) => ({ x: Math.sin(t * 7) * 0.3 + Math.cos(t * 11) * 0.2, y: Math.cos(t * 5) * 0.2 }) } ]; let hypnoVisualGroup = null; let hypnoEyePattern = 0; // Initialize hypnosis visual effects // v6.33: Made overlay much more subtle function initHypnosisEffects() { // Create overlay for trance effect - subtle edge vignette only const overlay = document.createElement('div'); overlay.id = 'hypnosis-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 48; background: radial-gradient(ellipse at center, transparent 50%, rgba(138, 43, 226, 0.05) 70%, rgba(255, 0, 255, 0.1) 85%, rgba(138, 43, 226, 0.2) 100%); opacity: 0; transition: opacity 0.5s; `; document.body.appendChild(overlay); HYPNOSIS_STATE.tranceOverlay = overlay; // Create spiral rings container in 3D hypnoVisualGroup = new THREE.Group(); } // Start hypnosis effect on player function startHypnosis(hypnotistMob, duration = 8000) { if (HYPNOSIS_STATE.active) return; HYPNOSIS_STATE.active = true; HYPNOSIS_STATE.hypnotistMob = hypnotistMob; HYPNOSIS_STATE.startTime = performance.now(); HYPNOSIS_STATE.duration = duration; HYPNOSIS_STATE.spiralAngle = 0; HYPNOSIS_STATE.eyePhase = 0; HYPNOSIS_STATE.breakAttempts = 0; // v6.13: Select random mind control effect const effectKeys = Object.keys(MIND_CONTROL_EFFECTS); const randomEffect = effectKeys[Math.floor(Math.random() * effectKeys.length)]; HYPNOSIS_STATE.mindControlEffect = randomEffect; HYPNOSIS_STATE.controlsInverted = (randomEffect === 'invertControls'); HYPNOSIS_STATE.lastMindControlAction = performance.now(); HYPNOSIS_STATE.resourcesDrained = 0; HYPNOSIS_STATE.attackedAllies = 0; const effectData = MIND_CONTROL_EFFECTS[randomEffect]; // Random eye pattern hypnoEyePattern = Math.floor(Math.random() * HYPNO_EYE_PATTERNS.length); // Show overlay if (HYPNOSIS_STATE.tranceOverlay) { HYPNOSIS_STATE.tranceOverlay.style.opacity = '1'; } // Create 3D spiral rings around player createHypnoSpirals(); // Show UI notification showHypnosisUI(); // v6.13: Show mind control effect notification if (typeof showNotification === 'function') { showNotification(`${effectData.icon} MIND CONTROL: ${effectData.message}`, 'error'); } // Copilot warning about the specific effect if (typeof addCopilotMessage === 'function') { addCopilotMessage(`⚠️ ALERT: Hypnotist is using ${effectData.name}! Press SPACE rapidly to break free!`, 'ai'); } console.log('Hypnosis started with effect:', randomEffect); } // Create swirling spiral rings effect // v6.33: Reduced from 5 rings to 2, smaller and less intrusive function createHypnoSpirals() { if (!scene || !worldState.player) return; // Clear existing HYPNOSIS_STATE.spiralRings.forEach(ring => { if (ring.parent) ring.parent.remove(ring); ring.geometry?.dispose(); ring.material?.dispose(); }); HYPNOSIS_STATE.spiralRings = []; // Create just 2 subtle rings around the player for (let i = 0; i < 2; i++) { const radius = 2 + i * 1.5; const ringGeo = new THREE.TorusGeometry(radius, 0.08, 8, 48); const ringMat = new THREE.MeshBasicMaterial({ color: i % 2 === 0 ? 0xff00ff : 0x8a2be2, transparent: true, opacity: 0.35 - i * 0.1, side: THREE.DoubleSide }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.rotation.x = Math.PI / 2; ring.position.y = 0.5 + i * 0.3; ring.userData.baseY = ring.position.y; ring.userData.index = i; scene.add(ring); HYPNOSIS_STATE.spiralRings.push(ring); } } // Update hypnosis effects each frame function updateHypnosis(dt) { if (!HYPNOSIS_STATE.active) return; const elapsed = performance.now() - HYPNOSIS_STATE.startTime; const progress = elapsed / HYPNOSIS_STATE.duration; // Check if hypnosis ended naturally if (progress >= 1) { endHypnosis(false); return; } // Update spiral angle HYPNOSIS_STATE.spiralAngle += dt * 2; HYPNOSIS_STATE.eyePhase += dt; // v8.17: forEach-to-for loop conversion for spiral rings animation (hot path) if (worldState.player) { const playerPos = worldState.player.position; const spiralRings = HYPNOSIS_STATE.spiralRings; for (let ri = 0, rlen = spiralRings.length; ri < rlen; ri++) { const ring = spiralRings[ri]; ring.position.x = playerPos.x; ring.position.z = playerPos.z; ring.position.y = playerPos.y + ring.userData.baseY + Math.sin(HYPNOSIS_STATE.spiralAngle + ri) * 0.5; ring.rotation.z = HYPNOSIS_STATE.spiralAngle * (ri % 2 === 0 ? 1 : -1) * 0.5; // Pulsing opacity ring.material.opacity = (0.4 + Math.sin(HYPNOSIS_STATE.spiralAngle * 2 + ri) * 0.2) * (1 - progress * 0.3); } } // Update companion eye animation updateHypnoEyeAnimation(); // Move player in trance pattern (toward hypnotist slowly) if (worldState.player && HYPNOSIS_STATE.hypnotistMob && !autopilotEnabled) { const hypnotistPos = HYPNOSIS_STATE.hypnotistMob.position; const playerPos = worldState.player.position; // Spiral movement toward hypnotist const angle = HYPNOSIS_STATE.spiralAngle * 0.3; const spiralRadius = 2 + Math.sin(HYPNOSIS_STATE.spiralAngle * 0.5) * 1; const targetX = hypnotistPos.x + Math.cos(angle) * spiralRadius; const targetZ = hypnotistPos.z + Math.sin(angle) * spiralRadius; // Very slow drift toward hypnotist playerPos.x += (targetX - playerPos.x) * dt * 0.3; playerPos.z += (targetZ - playerPos.z) * dt * 0.3; } // Overlay pulsing - v6.33: more subtle if (HYPNOSIS_STATE.tranceOverlay) { const pulse = 0.4 + Math.sin(HYPNOSIS_STATE.spiralAngle * 2) * 0.15; HYPNOSIS_STATE.tranceOverlay.style.opacity = pulse.toString(); } // ========================================== // v6.13: MIND CONTROL BEHAVIORAL EFFECTS // Robot performs involuntary actions based on hypnotist's agenda // ========================================== const now = performance.now(); const timeSinceLastAction = now - HYPNOSIS_STATE.lastMindControlAction; const actionInterval = 1500; // Involuntary action every 1.5 seconds if (timeSinceLastAction >= actionInterval && HYPNOSIS_STATE.mindControlEffect) { HYPNOSIS_STATE.lastMindControlAction = now; switch (HYPNOSIS_STATE.mindControlEffect) { case 'resourceDrain': // Robot drops resources involuntarily applyResourceDrainEffect(); break; case 'friendlyFire': // Robot might attack nearby allies (agents) applyFriendlyFireEffect(); break; case 'selfSabotage': // Robot damages itself or loses progress applySelfSabotageEffect(); break; // invertControls is handled in movement code } } } // v6.13: Resource Drain - Robot drops items from inventory // v8.25: Added defensive guard function applyResourceDrainEffect() { // v8.25: Guard against undefined gameData if (!gameData || !gameData.inventory || gameData.inventory.length === 0) return; // Find a random item to drop const filledSlots = gameData.inventory.filter(item => item && item.name); if (filledSlots.length === 0) return; const randomItem = filledSlots[Math.floor(Math.random() * filledSlots.length)]; const itemIdx = gameData.inventory.indexOf(randomItem); if (itemIdx >= 0 && randomItem.amount > 0) { // Reduce amount or remove item randomItem.amount--; if (randomItem.amount <= 0) { gameData.inventory.splice(itemIdx, 1); } HYPNOSIS_STATE.resourcesDrained++; // Visual feedback // v8.24: Use getFloaterPos() instead of clone() allocation if (worldState.player) { const dropPos = getFloaterPos(worldState.player.position, 1.5); spawnFloater(dropPos, `💸 Dropped: ${randomItem.name}`, '#ffaa00'); if (particles) { particles.emit(dropPos, 10, 0xffaa00, { spread: 3, lifetime: 600 }); } } updateInventoryUI(); } } // v6.13: Friendly Fire - Robot might attack nearby agents // v7.77: Use distanceToSquared to eliminate sqrt calls function applyFriendlyFireEffect() { if (!worldState.agents || worldState.agents.length === 0) return; // 40% chance to actually attack if (Math.random() > 0.4) return; // Find nearest agent // v7.77: Use squared distance for efficiency let nearestAgent = null; let nearestDistSq = 225; // 15 * 15 for (const agent of worldState.agents) { if (!agent.mesh || !agent.active) continue; const distSq = agent.mesh.position.distanceToSquared(worldState.player.position); if (distSq < nearestDistSq) { nearestDistSq = distSq; nearestAgent = agent; } } if (nearestAgent && nearestAgent.mesh) { // Deal damage to the agent const damage = Math.floor(5 + Math.random() * 10); if (nearestAgent.hp !== undefined) { nearestAgent.hp = Math.max(0, nearestAgent.hp - damage); } HYPNOSIS_STATE.attackedAllies++; // Visual feedback // v8.24: Use getFloaterPos() instead of clone() allocation const agentPos = getFloaterPos(nearestAgent.mesh.position, 0); spawnFloater(agentPos, `🎯 Confused Attack! -${damage}`, '#ff4444'); if (particles) { particles.emit(agentPos, 15, 0xff4444, { spread: 2, lifetime: 500 }); } // Copilot warning if (typeof addCopilotMessage === 'function' && Math.random() < 0.5) { addCopilotMessage(`⚠️ Commander! You're attacking our own units! Break free!`, 'ai'); } } } // v6.13: Self Sabotage - Robot takes self-damage or loses XP // v8.25: Added defensive guard function applySelfSabotageEffect() { // v8.25: Guard against undefined gameData.player if (!gameData || !gameData.player) return; // 60% chance to take damage, 40% chance to lose XP if (Math.random() < 0.6) { // Self-damage const damage = Math.floor(3 + Math.random() * 7); gameData.player.hp = Math.max(1, gameData.player.hp - damage); if (worldState.player) { spawnFloater(worldState.player.position, `⚠️ Malfunction! -${damage}`, '#ff6600'); if (particles) { particles.emit(worldState.player.position, 12, 0xff6600, { spread: 2, lifetime: 400 }); } } updateHealthUI(); } else { // Lose XP const xpLoss = Math.floor(5 + Math.random() * 15); gameData.player.xp = Math.max(0, gameData.player.xp - xpLoss); if (worldState.player) { spawnFloater(worldState.player.position, `⚠️ Memory Leak! -${xpLoss} XP`, '#ff6600'); } updateXPUI(); } } // Update the "eye" animation on the companion orb function updateHypnoEyeAnimation() { if (!copilotMesh || !HYPNOSIS_STATE.active) return; const pattern = HYPNO_EYE_PATTERNS[hypnoEyePattern]; const offset = pattern.fn(HYPNOSIS_STATE.eyePhase); HYPNOSIS_STATE.companionEyeOffset = offset; // Move the companion's inner orb to create "eye looking around" effect const orb = copilotMesh.userData?.orb; const core = copilotMesh.userData?.core; if (orb) { orb.position.x = offset.x; orb.position.y = offset.y; } if (core) { core.position.x = offset.x * 0.5; core.position.y = offset.y * 0.5; } // Also make the companion face the hypnotist if (HYPNOSIS_STATE.hypnotistMob) { copilotMesh.lookAt(HYPNOSIS_STATE.hypnotistMob.position); } } // Show hypnosis UI // v6.33: Made less intrusive - smaller, positioned at top instead of center function showHypnosisUI() { // Create or show the break-free UI let hypnoUI = document.getElementById('hypnosis-ui'); if (!hypnoUI) { hypnoUI = document.createElement('div'); hypnoUI.id = 'hypnosis-ui'; hypnoUI.style.cssText = ` position: fixed; top: 120px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.75); border: 2px solid #ff00ff; border-radius: 10px; padding: 12px 24px; color: #fff; font-family: Georgia, serif; text-align: center; z-index: 1005; animation: hypnoPulse 1.5s ease-in-out infinite; `; hypnoUI.innerHTML = `
👁️
HYPNOTIZED
Press SPACE to break free
0 / ${HYPNOSIS_STATE.maxBreakAttempts}
`; document.body.appendChild(hypnoUI); // Add subtle pulse animation const style = document.createElement('style'); style.textContent = ` @keyframes hypnoPulse { 0%, 100% { box-shadow: 0 0 10px rgba(255, 0, 255, 0.3); } 50% { box-shadow: 0 0 20px rgba(255, 0, 255, 0.5); } } `; document.head.appendChild(style); } hypnoUI.style.display = 'block'; } // Handle break attempt (called when player presses SPACE during hypnosis) function attemptBreakHypnosis() { if (!HYPNOSIS_STATE.active) return; HYPNOSIS_STATE.breakAttempts++; // Update UI const fill = document.getElementById('hypno-break-fill'); const text = document.getElementById('hypno-break-text'); if (fill) { fill.style.width = (HYPNOSIS_STATE.breakAttempts / HYPNOSIS_STATE.maxBreakAttempts * 100) + '%'; } if (text) { text.textContent = `${HYPNOSIS_STATE.breakAttempts} / ${HYPNOSIS_STATE.maxBreakAttempts}`; } // Screen flash on attempt if (HYPNOSIS_STATE.tranceOverlay) { HYPNOSIS_STATE.tranceOverlay.style.background = 'radial-gradient(ellipse at center, rgba(0,255,255,0.3) 0%, rgba(138,43,226,0.4) 100%)'; setTimeout(() => { if (HYPNOSIS_STATE.tranceOverlay) { HYPNOSIS_STATE.tranceOverlay.style.background = 'radial-gradient(ellipse at center, transparent 20%, rgba(138, 43, 226, 0.1) 40%, rgba(255, 0, 255, 0.2) 60%, rgba(138, 43, 226, 0.4) 100%)'; } }, 100); } // Check if broken free if (HYPNOSIS_STATE.breakAttempts >= HYPNOSIS_STATE.maxBreakAttempts) { endHypnosis(true); } } // End hypnosis effect function endHypnosis(brokeFreeBySelf) { if (!HYPNOSIS_STATE.active) return; HYPNOSIS_STATE.active = false; // Hide overlay if (HYPNOSIS_STATE.tranceOverlay) { HYPNOSIS_STATE.tranceOverlay.style.opacity = '0'; } // Remove spiral rings // v8.24: Use for loop instead of forEach for consistency const spiralRings = HYPNOSIS_STATE.spiralRings; for (let i = 0; i < spiralRings.length; i++) { const ring = spiralRings[i]; if (ring.parent) ring.parent.remove(ring); ring.geometry?.dispose(); ring.material?.dispose(); } HYPNOSIS_STATE.spiralRings = []; // Reset companion eye position if (copilotMesh) { const orb = copilotMesh.userData?.orb; const core = copilotMesh.userData?.core; if (orb) { orb.position.x = 0; orb.position.y = 0; } if (core) { core.position.x = 0; core.position.y = 0; } } // Hide UI const hypnoUI = document.getElementById('hypnosis-ui'); if (hypnoUI) hypnoUI.style.display = 'none'; // If broke free, damage the hypnotist! if (brokeFreeBySelf && HYPNOSIS_STATE.hypnotistMob) { const damage = HYPNOSIS_STATE.breakDamage; HYPNOSIS_STATE.hypnotistMob.userData.hp -= damage; // Show damage effect // v8.24: Use getFloaterPos() instead of clone() allocation if (typeof createDamageNumber === 'function') { createDamageNumber(getFloaterPos(HYPNOSIS_STATE.hypnotistMob.position, 0), damage, 0x00ffff); } // Notification if (typeof showNotification === 'function') { showNotification(`🔮 You broke free! Dealt ${damage} psychic damage to the Hypnotist!`, 'success'); } // Screen shake if (typeof screenShake === 'function') { screenShake(0.5); } // Check if hypnotist died if (HYPNOSIS_STATE.hypnotistMob.userData.hp <= 0) { // Handle death const xpReward = HYPNOSIS_STATE.hypnotistMob.userData.xpReward || 400; if (typeof addXp === 'function') addXp('combat', xpReward); gameData.statistics.mobsKilled++; if (typeof showNotification === 'function') { showNotification('👁️ Hypnotist defeated by psychic backlash!', 'success'); } // Remove from scene if (HYPNOSIS_STATE.hypnotistMob.parent) { scene.remove(HYPNOSIS_STATE.hypnotistMob); } const idx = worldState.mobs.indexOf(HYPNOSIS_STATE.hypnotistMob); if (idx > -1) worldState.mobs.splice(idx, 1); } } else if (!brokeFreeBySelf) { // Hypnosis wore off naturally - player takes some damage if (worldState.player) { const damage = 10; gameData.player.hp = Math.max(1, gameData.player.hp - damage); // v6.41: Fixed wrong property access if (typeof updateHealthUI === 'function') updateHealthUI(); if (typeof showNotification === 'function') { showNotification('👁️ Hypnosis wore off. You feel drained...', 'warning'); } } } HYPNOSIS_STATE.hypnotistMob = null; } // v4.7: Elite Enemy System - Affixes that modify enemy behavior const ELITE_AFFIXES = { swift: { name: 'Swift', prefix: '⚡', color: 0x00ffff, speedMult: 1.8, hpMult: 1.2, damageMult: 1.0, description: 'Moves much faster' }, armored: { name: 'Armored', prefix: '🛡️', color: 0x888888, speedMult: 0.8, hpMult: 3.0, damageMult: 1.0, description: 'Extremely tough' }, vampiric: { name: 'Vampiric', prefix: '🦇', color: 0x990000, speedMult: 1.0, hpMult: 1.5, damageMult: 1.2, lifesteal: 0.3, description: 'Heals on hit' }, explosive: { name: 'Explosive', prefix: '💥', color: 0xff6600, speedMult: 1.0, hpMult: 1.5, damageMult: 0.8, explodeOnDeath: true, description: 'Explodes on death' }, berserker: { name: 'Berserker', prefix: '😤', color: 0xff0000, speedMult: 1.2, hpMult: 1.0, damageMult: 2.0, description: 'Deals double damage' }, regenerating: { name: 'Regenerating', prefix: '💚', color: 0x00ff00, speedMult: 1.0, hpMult: 1.8, damageMult: 1.0, regenRate: 0.02, description: 'Regenerates health' }, teleporter: { name: 'Teleporter', prefix: '🌀', color: 0x9900ff, speedMult: 0.9, hpMult: 1.3, damageMult: 1.3, canTeleport: true, description: 'Blinks around' }, frozen: { name: 'Frozen', prefix: '❄️', color: 0x88ddff, speedMult: 0.7, hpMult: 2.0, damageMult: 1.1, chillingAura: true, description: 'Slows nearby player' } }; const ELITE_CONFIG = { spawnChance: 0.15, // 15% chance for elite minWorldLevel: 2, // Only spawn in world level 2+ essenceDropChance: 0.8, // 80% chance to drop elite essence bonusXpMult: 2.5, // 2.5x XP from elites bonusDropMult: 2 // Double drops from elites }; // v4.7: Session Rewards - Welcome back bonuses const SESSION_REWARDS = { tiers: [ { minHours: 1, xpBonus: 50, resources: { 'Slime': 2 }, message: 'Quick break bonus!' }, { minHours: 4, xpBonus: 150, resources: { 'Ore': 3, 'Log': 3 }, message: 'Gone a while bonus!' }, { minHours: 12, xpBonus: 400, resources: { 'Ore': 8, 'Log': 8, 'Health Potion': 2 }, message: 'Half-day bonus!' }, { minHours: 24, xpBonus: 1000, resources: { 'Crystal': 2, 'Mystic Orb': 1, 'Health Potion': 3 }, message: 'Daily login bonus!' }, { minHours: 72, xpBonus: 3000, resources: { 'Elite Essence': 5, 'Legendary Core': 1, 'Super Potion': 2 }, message: 'We missed you bonus!' } ], maxOfflineHours: 168 // Cap at 1 week }; // v4.6: Get equipped weapon element function getEquippedElement() { const weapons = ['Legendary Blade', 'Void Dagger', 'Magma Sword', 'Frost Blade']; for (const weapon of weapons) { if (hasItem(weapon)) { return ITEMS[weapon].element || null; } } return null; } // v4.6: Apply status effect to mob // v8.25: Added input validation for robustness function applyStatusEffect(mob, element) { // v8.25: Input validation - early return for invalid inputs if (!mob || !mob.userData) return; if (!element || typeof element !== 'string') return; const effect = STATUS_EFFECTS[element]; if (!effect) return; const data = mob.userData; data.statusEffects = data.statusEffects || {}; // Only apply if not already affected by this element if (data.statusEffects[element]) return; data.statusEffects[element] = { endTime: performance.now() + effect.duration, lastTick: performance.now() }; // Apply immediate effects if (effect.speedMod) { data.speedMultiplier = (data.speedMultiplier || 1) * effect.speedMod; } if (effect.damageMod) { data.damageMultiplier = (data.damageMultiplier || 1) * effect.damageMod; } // Visual feedback // v10.12: Added emissive check if (mob.material?.emissive) mob.material.emissive.setHex(effect.color); spawnFloater(mob.position, effect.icon + ' ' + effect.name, '#' + effect.color.toString(16).padStart(6, '0')); AudioSystem.hit(); } // v4.6: Update status effects for mob // v8.25: Added input validation for robustness function updateMobStatusEffects(mob, time) { // v8.25: Input validation - early return for invalid inputs if (!mob || !mob.userData) return; if (typeof time !== 'number' || !isFinite(time)) return; const data = mob.userData; if (!data.statusEffects) return; for (const [element, state] of Object.entries(data.statusEffects)) { const effect = STATUS_EFFECTS[element]; if (!effect) continue; // Apply DoT if (effect.tickDamage && time - state.lastTick >= effect.tickRate) { data.hp -= effect.tickDamage; state.lastTick = time; spawnFloater(mob.position, `-${effect.tickDamage}`, '#' + effect.color.toString(16).padStart(6, '0')); // Check for death by status effect if (data.hp <= 0) { // Will be handled in main mob loop } } // Check expiration if (time >= state.endTime) { // v6.8: Clear effects with division-by-zero safety (Agent consensus - Bug Fix) if (effect.speedMod && effect.speedMod !== 0) { data.speedMultiplier = (data.speedMultiplier || 1) / effect.speedMod; // Sanity clamp to prevent invalid values data.speedMultiplier = Math.max(0.1, Math.min(10, data.speedMultiplier)); } if (effect.damageMod && effect.damageMod !== 0) { data.damageMultiplier = (data.damageMultiplier || 1) / effect.damageMod; // Sanity clamp to prevent invalid values data.damageMultiplier = Math.max(0.1, Math.min(10, data.damageMultiplier)); } delete data.statusEffects[element]; // Restore emissive color if no more effects // v10.12: Added emissive check if (Object.keys(data.statusEffects).length === 0 && mob.material?.emissive) { const originalEmissive = ENEMY_TYPES[data.name]?.emissive || 0x003300; mob.material.emissive.setHex(originalEmissive); } } } } // v4.3: Boss Encounter System // v4.5: Added gear check requirements and increased mob kill thresholds const BOSS_TYPES = { 'Terra_Boss': { name: 'Ancient Guardian', hp: 100, damage: 15, speed: 2, scale: 2.5, color: 0x228b22, emissive: 0x114411, drops: [{ item: 'Boss Trophy', count: 1 }, { item: 'Ancient Artifact', count: 3 }], xp: 1000, biome: 'Terra', spawnCondition: { mobsKilled: 8, minCombatLevel: 2 }, attackWindup: 1000, attackRange: 4 }, 'Desert_Boss': { name: 'Sandstorm Titan', hp: 120, damage: 18, speed: 3, scale: 2.8, color: 0xcc8844, emissive: 0x664422, drops: [{ item: 'Boss Trophy', count: 1 }, { item: 'Chitin', count: 10 }], xp: 1200, biome: 'Desert', spawnCondition: { mobsKilled: 10, minCombatLevel: 3, requiredItem: 'Sword' }, attackWindup: 900, attackRange: 4.5 }, 'Ice_Boss': { name: 'Frost Monarch', hp: 90, damage: 20, speed: 4, scale: 2.2, color: 0x88ddff, emissive: 0x4488aa, drops: [{ item: 'Boss Trophy', count: 1 }, { item: 'Frost Shard', count: 10 }], xp: 1100, biome: 'Ice', spawnCondition: { mobsKilled: 10, minCombatLevel: 4 }, attackWindup: 700, attackRange: 5 }, 'Volcanic_Boss': { name: 'Magma Colossus', hp: 150, damage: 25, speed: 1.5, scale: 3, color: 0xff4400, emissive: 0xaa2200, drops: [{ item: 'Boss Trophy', count: 1 }, { item: 'Magma Gem', count: 10 }], xp: 1500, biome: 'Volcanic', spawnCondition: { mobsKilled: 12, minCombatLevel: 5, requiredItem: 'Frost Blade' }, attackWindup: 1500, attackRange: 5 }, 'Alien_Boss': { name: 'Void Leviathan', hp: 200, damage: 30, speed: 3, scale: 3.5, color: 0x8800ff, emissive: 0x440088, drops: [{ item: 'Boss Trophy', count: 2 }, { item: 'Void Fragment', count: 15 }, { item: 'Legendary Core', count: 1 }], xp: 2500, biome: 'Alien', spawnCondition: { mobsKilled: 15, minCombatLevel: 7, requiredItem: 'Magma Sword' }, attackWindup: 800, attackRange: 6 } }; // v4.2: Points of Interest System const POI_TYPES = { 'ancient_ruins': { name: 'Ancient Ruins', icon: '🏛️', rarity: 0.12, rewards: [{ item: 'Ancient Artifact', count: 1 }], xpBonus: 200, biomes: null }, 'crystal_cave': { name: 'Crystal Cavern', icon: '💎', rarity: 0.10, rewards: [{ item: 'Crystal', count: [2, 5] }], xpBonus: 150, biomes: ['Ice', 'Alien'] }, 'oasis': { name: 'Hidden Oasis', icon: '🌴', rarity: 0.15, rewards: [{ item: 'Healing Spring', count: 1 }], xpBonus: 100, biomes: ['Desert'] }, 'volcano_vent': { name: 'Volcanic Vent', icon: '🌋', rarity: 0.12, rewards: [{ item: 'Obsidian', count: [1, 3] }], xpBonus: 175, biomes: ['Volcanic'] }, 'crashed_ship': { name: 'Crashed Vessel', icon: '🛸', rarity: 0.06, rewards: [{ item: 'Tech Fragment', count: 1 }, { item: 'Power Cell', count: 1 }], xpBonus: 300, biomes: null }, 'mystic_shrine': { name: 'Mystic Shrine', icon: '⛩️', rarity: 0.08, rewards: [{ item: 'Mystic Orb', count: 1 }], xpBonus: 250, biomes: ['Terra', 'Alien'] } }; // v4.2: Player Ranks and Titles const PLAYER_RANKS = [ { points: 0, title: 'Novice Explorer', color: '#888888' }, { points: 100, title: 'Wanderer', color: '#44ff44' }, { points: 500, title: 'Pathfinder', color: '#4488ff' }, { points: 1500, title: 'Star Scout', color: '#ff8844' }, { points: 5000, title: 'Galaxy Ranger', color: '#ff44ff' }, { points: 15000, title: 'Cosmic Legend', color: '#ffd700' } ]; const SPECIAL_TITLES = { 'Slime Bane': { condition: (s, sk) => s.mobsKilled >= 100, color: '#ff4444' }, 'Master Lumberjack': { condition: (s, sk) => sk.wood.level >= 10, color: '#44aa44' }, 'Deep Miner': { condition: (s, sk) => sk.mining.level >= 10, color: '#888888' }, 'Cosmic Wanderer': { condition: (s, sk) => gameData.visitedPlanets.length >= 50, color: '#00ffff' }, 'Combat Master': { condition: (s, sk) => sk.combat.level >= 10, color: '#ff6644' }, 'Master Angler': { condition: (s, sk) => sk.fishing.level >= 10, color: '#4488ff' } }; const ITEMS = { // Base resources 'Log': { icon: '🪵', stackable: true, maxStack: 99 }, 'Ore': { icon: '🪨', stackable: true, maxStack: 99 }, 'Slime': { icon: '🟢', stackable: true, maxStack: 99 }, 'Raw Fish': { icon: '🐟', stackable: true, maxStack: 99 }, 'Cooked Fish': { icon: '🍖', stackable: true, maxStack: 99, heal: 20 }, // v4.2: Biome-specific enemy drops 'Chitin': { icon: '🦂', stackable: true, maxStack: 99 }, 'Frost Shard': { icon: '❄️', stackable: true, maxStack: 99 }, 'Magma Gem': { icon: '🔥', stackable: true, maxStack: 99 }, 'Void Fragment': { icon: '🌀', stackable: true, maxStack: 99 }, // v4.2: POI rewards 'Ancient Artifact': { icon: '🏺', stackable: true, maxStack: 20 }, 'Crystal': { icon: '💠', stackable: true, maxStack: 50 }, 'Healing Spring': { icon: '💧', stackable: true, maxStack: 10, heal: 100 }, 'Obsidian': { icon: '🖤', stackable: true, maxStack: 50 }, 'Tech Fragment': { icon: '🔧', stackable: true, maxStack: 20 }, 'Power Cell': { icon: '🔋', stackable: true, maxStack: 10 }, 'Mystic Orb': { icon: '🔮', stackable: true, maxStack: 10 }, // Tools 'Pickaxe': { icon: '⛏️', stackable: false, miningBonus: 2 }, 'Sword': { icon: '🗡️', stackable: false, combatBonus: 5 }, 'Fishing Rod': { icon: '🎣', stackable: false, fishingBonus: 2 }, 'Health Potion': { icon: '🧪', stackable: true, maxStack: 10, heal: 50 }, // v4.2: New craftables 'Frost Blade': { icon: '🗡️', stackable: false, combatBonus: 8, element: 'ice' }, 'Magma Sword': { icon: '🗡️', stackable: false, combatBonus: 10, element: 'fire' }, 'Void Dagger': { icon: '🗡️', stackable: false, combatBonus: 12, element: 'void' }, 'Crystal Pickaxe': { icon: '⛏️', stackable: false, miningBonus: 3 }, 'Super Potion': { icon: '🧪', stackable: true, maxStack: 10, heal: 100 }, 'Chitin Armor': { icon: '🛡️', stackable: false, defenseBonus: 5 }, // v4.3: Boss rewards 'Boss Trophy': { icon: '🏆', stackable: true, maxStack: 20 }, 'Legendary Core': { icon: '💎', stackable: true, maxStack: 5 }, // v4.3: Legendary gear (requires boss materials) 'Legendary Blade': { icon: '⚔️', stackable: false, combatBonus: 20, element: 'cosmic' }, 'Guardian Armor': { icon: '🛡️', stackable: false, defenseBonus: 15 }, // v4.7: Elite enemy drops 'Elite Essence': { icon: '💠', stackable: true, maxStack: 99 }, 'Berserker Badge': { icon: '🔴', stackable: false, combatBonus: 15, attackSpeedMult: 1.3 }, 'Vampiric Fang': { icon: '🦷', stackable: false, combatBonus: 10, lifesteal: 0.15 }, 'Frost Heart': { icon: '💙', stackable: false, defenseBonus: 10, element: 'ice' }, // v5.1: New craftable equipment 'Iron Armor': { icon: '🛡️', stackable: false, defenseBonus: 3 }, 'Steel Armor': { icon: '🛡️', stackable: false, defenseBonus: 8 }, 'Lucky Charm': { icon: '🍀', stackable: false }, 'Swift Boots': { icon: '👢', stackable: false }, 'Power Ring': { icon: '💍', stackable: false }, 'Master Rod': { icon: '🎣', stackable: false, fishingBonus: 4 }, // v5.1: Enchantment materials 'Enchant Shard': { icon: '✨', stackable: true, maxStack: 50 }, 'Arcane Dust': { icon: '💫', stackable: true, maxStack: 99 }, // v5.3: Portal realm rewards 'Shadow Essence': { icon: '🌑', stackable: true, maxStack: 50 }, 'Dark Crystal': { icon: '🔮', stackable: true, maxStack: 30 }, 'Frozen Heart': { icon: '💙', stackable: true, maxStack: 30 }, 'Permafrost Shard': { icon: '❄️', stackable: true, maxStack: 50 }, 'Infernal Core': { icon: '🔥', stackable: true, maxStack: 30 }, 'Magma Heart': { icon: '❤️‍🔥', stackable: true, maxStack: 30 }, 'Void Core': { icon: '🌀', stackable: true, maxStack: 20 }, 'Dimension Shard': { icon: '💠', stackable: true, maxStack: 30 }, 'Celestial Essence': { icon: '✨', stackable: true, maxStack: 10 }, 'Star Fragment': { icon: '⭐', stackable: true, maxStack: 20 }, 'Mythic Orb': { icon: '🔮', stackable: false, combatBonus: 25, element: 'cosmic', defenseBonus: 10 }, // v5.4: World Event items 'Meteor Ore': { icon: '☄️', stackable: true, maxStack: 30, description: 'Rare ore from a meteor shower' }, 'Cosmic Dust': { icon: '🌟', stackable: true, maxStack: 99, description: 'Glittering cosmic particles' }, 'Gold Chest': { icon: '📦', stackable: true, maxStack: 10, description: 'A treasure chest filled with gold' }, 'Silver Chest': { icon: '📦', stackable: true, maxStack: 20, description: 'A treasure chest with silver' }, 'Ancient Relic': { icon: '🗿', stackable: true, maxStack: 10, description: 'An ancient relic of power' }, 'Rune Stone': { icon: '🪨', stackable: true, maxStack: 20, description: 'Stone inscribed with ancient runes' }, 'Lost Technology': { icon: '🔧', stackable: true, maxStack: 10, description: 'Advanced technology from a lost civilization' }, 'Rainbow Crystal': { icon: '💎', stackable: true, maxStack: 15, description: 'A crystal that shimmers with all colors' }, 'Pure Crystal': { icon: '💠', stackable: true, maxStack: 20, description: 'A perfectly pure crystal' }, 'Crystal Shard': { icon: '🔹', stackable: true, maxStack: 50, description: 'A small crystal fragment' }, // v6.68: SET ITEMS - Equipment that grants bonuses when worn together // Voidwalker Set 'Void Cloak': { icon: '🧥', stackable: false, defenseBonus: 12, element: 'void', description: 'A cloak woven from void energy' }, 'Void Ring': { icon: '💍', stackable: false, combatBonus: 8, element: 'void', description: 'A ring that pulses with void power' }, // Inferno Set 'Inferno Plate': { icon: '🔥', stackable: false, defenseBonus: 15, element: 'fire', description: 'Armor forged in volcanic fire' }, 'Flame Ring': { icon: '💍', stackable: false, combatBonus: 10, element: 'fire', description: 'A ring burning with eternal flame' }, // Frostborne Set 'Frost Armor': { icon: '❄️', stackable: false, defenseBonus: 14, element: 'ice', description: 'Armor made of enchanted permafrost' }, // Berserker Set 'Berserker Helm': { icon: '⛑️', stackable: false, defenseBonus: 8, combatBonus: 5, description: 'A helm that amplifies rage' }, 'Berserker Gauntlets': { icon: '🧤', stackable: false, combatBonus: 12, attackSpeedMult: 1.1, description: 'Gauntlets that never tire' }, // Guardian Set 'Guardian Shield': { icon: '🛡️', stackable: false, defenseBonus: 20, description: 'An impenetrable shield of light' }, 'Guardian Helm': { icon: '⛑️', stackable: false, defenseBonus: 12, description: 'A helm blessed by guardians' }, // Harvester Set 'Harvester Vest': { icon: '🧥', stackable: false, defenseBonus: 5, description: 'A vest with many pockets for resources' }, // Celestial Set 'Celestial Armor': { icon: '✨', stackable: false, defenseBonus: 25, combatBonus: 10, element: 'cosmic', description: 'Armor woven from starlight' } }; const RECIPES = { 'pickaxe': { result: 'Pickaxe', requires: { 'Ore': 3, 'Log': 2 } }, 'sword': { result: 'Sword', requires: { 'Ore': 5, 'Log': 1 } }, 'rod': { result: 'Fishing Rod', requires: { 'Log': 2 } }, 'cookedFish': { result: 'Cooked Fish', requires: { 'Raw Fish': 1 } }, 'potion': { result: 'Health Potion', requires: { 'Slime': 2 } }, // v4.2: New recipes using biome materials 'frostBlade': { result: 'Frost Blade', requires: { 'Ore': 8, 'Frost Shard': 5 }, craftingLevel: 5 }, 'magmaSword': { result: 'Magma Sword', requires: { 'Ore': 10, 'Magma Gem': 5 }, craftingLevel: 7 }, 'voidDagger': { result: 'Void Dagger', requires: { 'Ore': 12, 'Void Fragment': 5 }, craftingLevel: 10 }, 'crystalPickaxe': { result: 'Crystal Pickaxe', requires: { 'Ore': 6, 'Crystal': 3 }, craftingLevel: 6 }, 'superPotion': { result: 'Super Potion', requires: { 'Slime': 3, 'Mystic Orb': 1 }, craftingLevel: 8 }, 'chitinArmor': { result: 'Chitin Armor', requires: { 'Chitin': 10, 'Log': 5 }, craftingLevel: 4 }, // v4.3: Legendary recipes (requires boss materials) 'legendaryBlade': { result: 'Legendary Blade', requires: { 'Boss Trophy': 5, 'Legendary Core': 1, 'Ore': 20 }, craftingLevel: 15 }, 'guardianArmor': { result: 'Guardian Armor', requires: { 'Boss Trophy': 3, 'Chitin': 20, 'Crystal': 5 }, craftingLevel: 12 }, // v4.7: Elite gear recipes 'berserkerBadge': { result: 'Berserker Badge', requires: { 'Elite Essence': 10, 'Magma Gem': 3 }, craftingLevel: 10 }, 'vampiricFang': { result: 'Vampiric Fang', requires: { 'Elite Essence': 15, 'Void Fragment': 5 }, craftingLevel: 12 }, 'frostHeart': { result: 'Frost Heart', requires: { 'Elite Essence': 12, 'Frost Shard': 8, 'Crystal': 3 }, craftingLevel: 11 }, // v5.1: New equipment recipes 'ironArmor': { result: 'Iron Armor', requires: { 'Ore': 8, 'Log': 3 }, craftingLevel: 2 }, 'steelArmor': { result: 'Steel Armor', requires: { 'Ore': 15, 'Crystal': 2 }, craftingLevel: 8 }, 'luckyCharm': { result: 'Lucky Charm', requires: { 'Crystal': 5, 'Mystic Orb': 2 }, craftingLevel: 6 }, 'swiftBoots': { result: 'Swift Boots', requires: { 'Chitin': 8, 'Slime': 5 }, craftingLevel: 5 }, 'powerRing': { result: 'Power Ring', requires: { 'Ore': 10, 'Magma Gem': 3 }, craftingLevel: 7 }, 'masterRod': { result: 'Master Rod', requires: { 'Log': 10, 'Crystal': 3, 'Frost Shard': 2 }, craftingLevel: 9 }, // v5.1: Enchantment material crafting 'enchantShard': { result: 'Enchant Shard', requires: { 'Crystal': 3, 'Mystic Orb': 1 }, craftingLevel: 8 }, 'arcaneDust': { result: 'Arcane Dust', requires: { 'Slime': 5, 'Void Fragment': 1 }, craftingLevel: 6 }, // v6.1: ALCHEMY RECIPES - New potion brewing system 'manaPotion': { result: 'Mana Potion', requires: { 'Crystal': 2, 'Slime': 1 }, alchemyLevel: 1, isAlchemy: true }, 'strengthElixir': { result: 'Strength Elixir', requires: { 'Magma Gem': 2, 'Slime': 2 }, alchemyLevel: 3, isAlchemy: true }, 'speedTonic': { result: 'Speed Tonic', requires: { 'Frost Shard': 2, 'Slime': 2 }, alchemyLevel: 3, isAlchemy: true }, 'defenseOil': { result: 'Defense Oil', requires: { 'Ore': 3, 'Slime': 3 }, alchemyLevel: 4, isAlchemy: true }, 'luckyDraught': { result: 'Lucky Draught', requires: { 'Mystic Orb': 1, 'Crystal': 2 }, alchemyLevel: 5, isAlchemy: true }, 'berserkerBrew': { result: 'Berserker Brew', requires: { 'Magma Gem': 3, 'Elite Essence': 2 }, alchemyLevel: 7, isAlchemy: true }, 'invisibilityPotion': { result: 'Invisibility Potion', requires: { 'Void Fragment': 3, 'Slime': 3 }, alchemyLevel: 8, isAlchemy: true }, 'phoenixTears': { result: 'Phoenix Tears', requires: { 'Legendary Core': 1, 'Magma Gem': 5, 'Crystal': 5 }, alchemyLevel: 12, isAlchemy: true }, 'transmutation': { result: 'Transmuted Ore', requires: { 'Log': 10 }, alchemyLevel: 2, isAlchemy: true }, 'antidote': { result: 'Antidote', requires: { 'Antidote Sample': 2, 'Slime': 1 }, alchemyLevel: 2, isAlchemy: true }, // v6.68: SET ITEM RECIPES - Craft complete sets for powerful bonuses // Voidwalker Set 'voidCloak': { result: 'Void Cloak', requires: { 'Void Fragment': 15, 'Shadow Essence': 10, 'Dimension Shard': 5 }, craftingLevel: 14 }, 'voidRing': { result: 'Void Ring', requires: { 'Void Fragment': 8, 'Void Core': 3, 'Crystal': 5 }, craftingLevel: 12 }, // Inferno Set 'infernoPlate': { result: 'Inferno Plate', requires: { 'Magma Gem': 20, 'Infernal Core': 5, 'Ore': 25 }, craftingLevel: 15 }, 'flameRing': { result: 'Flame Ring', requires: { 'Magma Gem': 10, 'Magma Heart': 3, 'Crystal': 5 }, craftingLevel: 11 }, // Frostborne Set 'frostArmor': { result: 'Frost Armor', requires: { 'Frost Shard': 20, 'Frozen Heart': 5, 'Permafrost Shard': 10 }, craftingLevel: 13 }, // Berserker Set 'berserkerHelm': { result: 'Berserker Helm', requires: { 'Elite Essence': 15, 'Magma Gem': 8, 'Ore': 15 }, craftingLevel: 13 }, 'berserkerGauntlets': { result: 'Berserker Gauntlets', requires: { 'Elite Essence': 20, 'Boss Trophy': 2, 'Ore': 12 }, craftingLevel: 14 }, // Guardian Set 'guardianShield': { result: 'Guardian Shield', requires: { 'Boss Trophy': 4, 'Crystal': 15, 'Ore': 30 }, craftingLevel: 16 }, 'guardianHelm': { result: 'Guardian Helm', requires: { 'Boss Trophy': 2, 'Crystal': 10, 'Chitin': 15 }, craftingLevel: 14 }, // Harvester Set 'harvesterVest': { result: 'Harvester Vest', requires: { 'Log': 20, 'Chitin': 15, 'Crystal': 5 }, craftingLevel: 8 }, // Celestial Set 'celestialArmor': { result: 'Celestial Armor', requires: { 'Celestial Essence': 5, 'Star Fragment': 15, 'Legendary Core': 2, 'Crystal': 20 }, craftingLevel: 20 } }; // v6.1: ALCHEMY POTION EFFECTS - Applied when consumed const POTION_EFFECTS = { 'Health Potion': { effect: 'heal', value: 30, duration: 0 }, 'Super Potion': { effect: 'heal', value: 75, duration: 0 }, 'Mana Potion': { effect: 'cooldownReset', value: 0.5, duration: 0 }, 'Strength Elixir': { effect: 'damage', value: 1.25, duration: 30000 }, 'Speed Tonic': { effect: 'speed', value: 1.3, duration: 25000 }, 'Defense Oil': { effect: 'defense', value: 1.4, duration: 30000 }, 'Lucky Draught': { effect: 'luck', value: 1.5, duration: 45000 }, 'Berserker Brew': { effect: 'berserk', value: 1.5, duration: 20000 }, // +50% damage, -20% defense 'Invisibility Potion': { effect: 'stealth', value: 1, duration: 15000 }, 'Phoenix Tears': { effect: 'revive', value: 1, duration: 0 } // Auto-revive on death }; // v6.1: Active potion buffs tracker let activePotionBuffs = {}; function consumePotion(potionName) { const effect = POTION_EFFECTS[potionName]; if (!effect) return false; const now = performance.now(); switch (effect.effect) { case 'heal': gameData.player.hp = Math.min(gameData.player.maxHp, gameData.player.hp + effect.value); updateHealthUI(); spawnFloater(worldState.player.position, `💚 +${effect.value} HP`, '#44ff44'); break; case 'cooldownReset': // Reset ability cooldowns by 50% Object.keys(abilityCooldowns).forEach(key => { abilityCooldowns[key] = Math.max(0, abilityCooldowns[key] - 5000); }); spawnFloater(worldState.player.position, `⚡ Cooldowns reduced!`, '#00ffff'); break; case 'damage': case 'speed': case 'defense': case 'luck': case 'berserk': case 'stealth': activePotionBuffs[effect.effect] = { value: effect.value, endTime: now + effect.duration, name: potionName }; spawnFloater(worldState.player.position, `✨ ${potionName} active!`, '#ff88ff'); showNotification(`${potionName} buff active for ${Math.floor(effect.duration / 1000)}s!`, 'buff'); break; case 'revive': activePotionBuffs.phoenixTears = { value: 1, endTime: now + 300000, name: potionName }; spawnFloater(worldState.player.position, `🔥 Phoenix protection active!`, '#ff8800'); showNotification('Phoenix Tears: You will auto-revive on death for 5 minutes!', 'buff'); break; } // Remove from inventory removeFromInventory(potionName, 1); AudioSystem.levelUp(); addXp('alchemy', 25); return true; } function getPotionBuffMultiplier(buffType) { const buff = activePotionBuffs[buffType]; if (!buff || performance.now() > buff.endTime) { delete activePotionBuffs[buffType]; return 1; } return buff.value; } // v8.06: Converted to for...in loop function updatePotionBuffs() { const now = performance.now(); let buffExpired = false; for (const key in activePotionBuffs) { if (now > activePotionBuffs[key].endTime) { showNotification(`${activePotionBuffs[key].name} buff expired`, 'info'); delete activePotionBuffs[key]; buffExpired = true; } } if (buffExpired) updateBuffsUI(); } // v7.73: DOM cache for buffs UI (performance optimization) let _buffsUICache = null; function getBuffsUICache() { if (!_buffsUICache) { _buffsUICache = { container: document.getElementById('active-buffs-container'), list: document.getElementById('active-buffs-list') }; } return _buffsUICache; } // v6.1: Update active buffs UI display // v7.73: Uses cached DOM refs for performance function updateBuffsUI() { const cache = getBuffsUICache(); const container = cache.container; const list = cache.list; if (!container || !list) return; const buffKeys = Object.keys(activePotionBuffs); if (buffKeys.length === 0) { container.style.display = 'none'; return; } container.style.display = 'block'; list.innerHTML = ''; const now = performance.now(); const buffIcons = { damage: '⚔️', speed: '💨', defense: '🛡️', luck: '🍀', berserk: '😤', stealth: '👻', phoenixTears: '🔥' }; // v8.17: forEach-to-for loop conversion for buff rendering for (let bi = 0, blen = buffKeys.length; bi < blen; bi++) { const key = buffKeys[bi]; const buff = activePotionBuffs[key]; const timeLeft = Math.max(0, Math.ceil((buff.endTime - now) / 1000)); const icon = buffIcons[key] || '✨'; const buffEl = document.createElement('div'); buffEl.style.cssText = 'background: rgba(255, 136, 255, 0.2); border: 1px solid #ff88ff; border-radius: 4px; padding: 2px 6px; font-size: 10px; display: flex; align-items: center; gap: 3px;'; buffEl.innerHTML = `${icon}${timeLeft}s`; buffEl.title = buff.name; list.appendChild(buffEl); } } // v5.1: Equipment System const EQUIPMENT_SLOTS = { weapon: { name: 'Weapon', icon: '⚔️', statKey: 'combatBonus' }, armor: { name: 'Armor', icon: '🛡️', statKey: 'defenseBonus' }, accessory: { name: 'Accessory', icon: '💍', statKey: 'special' }, tool: { name: 'Tool', icon: '🔧', statKey: 'toolBonus' } }; // v5.1: Map items to equipment slots const EQUIPMENT_MAP = { // Weapons 'Sword': { slot: 'weapon', stats: { damage: 5 } }, 'Frost Blade': { slot: 'weapon', stats: { damage: 8, element: 'ice' } }, 'Magma Sword': { slot: 'weapon', stats: { damage: 10, element: 'fire' } }, 'Void Dagger': { slot: 'weapon', stats: { damage: 12, element: 'void' } }, 'Legendary Blade': { slot: 'weapon', stats: { damage: 20, element: 'cosmic', critChance: 0.15 } }, // Armor (tiered) 'Iron Armor': { slot: 'armor', stats: { defense: 3 } }, 'Chitin Armor': { slot: 'armor', stats: { defense: 5 } }, 'Steel Armor': { slot: 'armor', stats: { defense: 8 } }, 'Guardian Armor': { slot: 'armor', stats: { defense: 15, maxHpBonus: 50 } }, // Accessories 'Berserker Badge': { slot: 'accessory', stats: { damage: 15, attackSpeed: 1.3 } }, 'Vampiric Fang': { slot: 'accessory', stats: { damage: 10, lifesteal: 0.15 } }, 'Frost Heart': { slot: 'accessory', stats: { defense: 10, element: 'ice' } }, 'Lucky Charm': { slot: 'accessory', stats: { critChance: 0.10, lootBonus: 0.15 } }, 'Swift Boots': { slot: 'accessory', stats: { moveSpeed: 1.15, dodgeBonus: 0.1 } }, 'Power Ring': { slot: 'accessory', stats: { damage: 8, critChance: 0.05 } }, // Tools 'Pickaxe': { slot: 'tool', stats: { miningBonus: 2 } }, 'Crystal Pickaxe': { slot: 'tool', stats: { miningBonus: 3 } }, 'Fishing Rod': { slot: 'tool', stats: { fishingBonus: 2 } }, 'Master Rod': { slot: 'tool', stats: { fishingBonus: 4 } } }; // ═══════════════════════════════════════════════════════════════ // v6.16: AUTO-CRAFT & AUTO-EQUIP SYSTEM // "Cream rises to the top" - automatically craft and equip best items // No manual inventory management needed // ═══════════════════════════════════════════════════════════════ // Item power rankings by slot (higher = better, auto-equips over lower) const ITEM_POWER = { // Weapons (by damage output) 'Sword': 5, 'Frost Blade': 10, 'Magma Sword': 15, 'Void Dagger': 18, 'Legendary Blade': 30, // Armor (by defense) 'Iron Armor': 5, 'Chitin Armor': 8, 'Steel Armor': 12, 'Guardian Armor': 25, // Accessories (by overall utility) 'Lucky Charm': 10, 'Swift Boots': 12, 'Power Ring': 14, 'Frost Heart': 16, 'Berserker Badge': 20, 'Vampiric Fang': 22, // Tools (by bonus effectiveness) 'Pickaxe': 5, 'Crystal Pickaxe': 12, 'Fishing Rod': 5, 'Master Rod': 15 }; // Recipe crafting priority (higher = craft first when materials available) const RECIPE_CRAFT_PRIORITY = { // Legendary tier (always craft if possible) 'legendaryBlade': 100, 'guardianArmor': 95, // Elite tier 'vampiricFang': 80, 'berserkerBadge': 75, 'frostHeart': 70, // High tier weapons/armor 'voidDagger': 60, 'steelArmor': 55, 'magmaSword': 50, 'masterRod': 45, // Mid tier 'frostBlade': 40, 'crystalPickaxe': 38, 'powerRing': 35, 'luckyCharm': 32, 'swiftBoots': 30, 'chitinArmor': 28, // Base tier (essential early game) 'ironArmor': 20, 'sword': 15, 'pickaxe': 10, 'rod': 8, // Consumables (lower priority, craft for sustain) 'superPotion': 6, 'potion': 4 }; // Check if a recipe can be crafted right now function canCraftRecipe(recipeId) { const recipe = RECIPES[recipeId]; if (!recipe) return false; // Check crafting level if (recipe.craftingLevel && gameData.skills.crafting.level < recipe.craftingLevel) { return false; } // Skip alchemy recipes (handled by alchemy system) if (recipe.isAlchemy) return false; // Check all required materials for (const [item, count] of Object.entries(recipe.requires)) { if (!hasItem(item, count)) return false; } return true; } // Check if item is currently equipped function isItemEquipped(itemName) { const gear = getEquippedGear(); return Object.values(gear).includes(itemName); } // Auto-craft the best available items (cream rises) function autoCraftBestItems() { // Sort recipes by priority (highest first) const sortedRecipes = Object.keys(RECIPE_CRAFT_PRIORITY) .sort((a, b) => RECIPE_CRAFT_PRIORITY[b] - RECIPE_CRAFT_PRIORITY[a]); let craftedSomething = false; for (const recipeId of sortedRecipes) { if (!canCraftRecipe(recipeId)) continue; const recipe = RECIPES[recipeId]; const resultName = recipe.result; const equipData = EQUIPMENT_MAP[resultName]; // For equipment: only craft if it's better than what we have if (equipData) { const slot = equipData.slot; const gear = getEquippedGear(); const currentItem = gear[slot]; const currentPower = currentItem ? (ITEM_POWER[currentItem] || 0) : 0; const newPower = ITEM_POWER[resultName] || 0; // Don't craft if we already have this or better equipped if (currentPower >= newPower) continue; // Don't craft duplicates in inventory if (countItem(resultName) > 0) continue; } // For consumables: maintain a small stock if (!equipData) { const currentCount = countItem(resultName); // Keep max 5 potions, don't over-craft if (currentCount >= 5) continue; } // Craft it! craft(recipeId, 1); craftedSomething = true; // Visual/audio feedback for auto-craft if (worldState.player) { spawnFloater(worldState.player.position, `🔨 Auto: ${resultName}`, '#a0f'); } break; // Only craft one thing per tick (pace the automation) } return craftedSomething; } // Auto-equip the best items in inventory (cream rises to top) function autoEquipBestItems() { const gear = getEquippedGear(); let equippedSomething = false; // Check each equipment slot for (const slotName of ['weapon', 'armor', 'accessory', 'tool']) { const currentItem = gear[slotName]; const currentPower = currentItem ? (ITEM_POWER[currentItem] || 0) : 0; // Find best unequipped item in inventory for this slot let bestItem = null; let bestPower = currentPower; // Scan inventory for better items const inventoryCounts = {}; gameData.inventory.forEach(itemName => { inventoryCounts[itemName] = (inventoryCounts[itemName] || 0) + 1; }); for (const itemName of Object.keys(inventoryCounts)) { const equipData = EQUIPMENT_MAP[itemName]; if (!equipData || equipData.slot !== slotName) continue; const power = ITEM_POWER[itemName] || 0; if (power > bestPower) { bestPower = power; bestItem = itemName; } } // Equip if we found something better if (bestItem) { // Silent equip (don't spam notifications during auto) const slot = getEquipmentSlot(bestItem); const gearRef = getEquippedGear(); // Return current item to inventory if any if (gearRef[slot]) { addItem(gearRef[slot]); } // Remove new item from inventory and equip if (removeItem(bestItem, 1)) { gearRef[slot] = bestItem; updateEquipmentUI(); saveGameData(); equippedSomething = true; // Visual feedback if (worldState.player) { spawnFloater(worldState.player.position, `⬆️ ${bestItem}`, '#4f4'); } AudioSystem.collect(); } } } return equippedSomething; } // Auto-craft/equip timer state let lastAutoCraftEquipTime = 0; const AUTO_CRAFT_EQUIP_INTERVAL = 2000; // Check every 2 seconds // Main auto-craft/equip runner (called from game loop) function runAutoCraftEquip(now) { // Only run when autopilot is enabled (matches player intent for automation) if (!autoExplore.enabled) return; // Throttle checks if (now - lastAutoCraftEquipTime < AUTO_CRAFT_EQUIP_INTERVAL) return; lastAutoCraftEquipTime = now; // First auto-equip (immediate upgrade) autoEquipBestItems(); // Then auto-craft (prepare for future) autoCraftBestItems(); } // v5.1: Equipment state getter (uses gameData for persistence) function getEquippedGear() { if (!gameData.equipment) { gameData.equipment = { weapon: null, armor: null, accessory: null, tool: null }; } return gameData.equipment; } // v5.1: Equipment functions function isEquippable(itemName) { return EQUIPMENT_MAP.hasOwnProperty(itemName); } function getEquipmentSlot(itemName) { return EQUIPMENT_MAP[itemName]?.slot || null; } function equipItem(itemName) { if (!isEquippable(itemName)) { showNotification('Cannot equip this item!', 'error'); return false; } const slot = getEquipmentSlot(itemName); const equipData = EQUIPMENT_MAP[itemName]; const gear = getEquippedGear(); // Unequip current item in slot (return to inventory) if (gear[slot]) { addItem(gear[slot]); showNotification(`Unequipped ${gear[slot]}`, 'info'); } // Remove from inventory if (!removeItem(itemName, 1)) { showNotification('Item not in inventory!', 'error'); return false; } // Equip new item gear[slot] = itemName; showNotification(`Equipped ${itemName}!`, 'success'); AudioSystem.collect(); updateEquipmentUI(); saveGameData(); return true; } function unequipItem(slot) { const gear = getEquippedGear(); if (!gear[slot]) return; const itemName = gear[slot]; if (gameData.inventory.length >= 20) { showNotification('Inventory full!', 'error'); return; } addItem(itemName); gear[slot] = null; showNotification(`Unequipped ${itemName}`, 'info'); updateEquipmentUI(); saveGameData(); } function getEquipmentStats() { const stats = { damage: 0, defense: 0, miningBonus: 0, fishingBonus: 0, attackSpeed: 1.0, lifesteal: 0, critChance: 0, maxHpBonus: 0, element: null, // v5.1: New stats moveSpeed: 1.0, lootBonus: 0, dodgeBonus: 0 }; const gear = getEquippedGear(); for (const slot of Object.keys(gear)) { const itemName = gear[slot]; if (!itemName) continue; const equipData = EQUIPMENT_MAP[itemName]; if (!equipData) continue; for (const [stat, value] of Object.entries(equipData.stats)) { if (stat === 'element') { stats.element = value; } else if (stat === 'attackSpeed' || stat === 'moveSpeed') { // Multiplicative stats stats[stat] *= value; } else { stats[stat] = (stats[stat] || 0) + value; } } // v5.1: Add enchantment bonuses const enchantBonuses = getEnchantmentBonuses(itemName); for (const [stat, value] of Object.entries(enchantBonuses)) { if (stat === 'moveSpeed') { stats[stat] *= value; // Multiplicative for move speed enchants } else { stats[stat] = (stats[stat] || 0) + value; } } } return stats; } // v7.73: DOM cache for equipment UI (performance optimization) let _equipUICache = null; function getEquipUICache() { if (!_equipUICache) { _equipUICache = { slots: {}, statsEl: document.getElementById('equipment-stats') }; // Cache each equipment slot and its sub-elements for (const slot of Object.keys(EQUIPMENT_SLOTS)) { const slotEl = document.getElementById(`equip-slot-${slot}`); if (slotEl) { _equipUICache.slots[slot] = { el: slotEl, icon: slotEl.querySelector('.equip-icon'), name: slotEl.querySelector('.equip-name') }; } } } return _equipUICache; } // v7.73: Uses cached DOM refs for performance function updateEquipmentUI() { const gear = getEquippedGear(); const cache = getEquipUICache(); for (const [slot, slotInfo] of Object.entries(EQUIPMENT_SLOTS)) { const cached = cache.slots[slot]; if (!cached || !cached.el) continue; const itemName = gear[slot]; const iconEl = cached.icon; const nameEl = cached.name; if (itemName) { const itemDef = ITEMS[itemName]; if (iconEl) iconEl.textContent = itemDef?.icon || '?'; if (nameEl) nameEl.textContent = itemName; cached.el.classList.add('equipped'); } else { if (iconEl) iconEl.textContent = slotInfo.icon; if (nameEl) nameEl.textContent = 'Empty'; cached.el.classList.remove('equipped'); } } // Update stats display const stats = getEquipmentStats(); const statsEl = cache.statsEl; if (statsEl) { let html = `
⚔️ +${stats.damage} DMG
🛡️ +${stats.defense} DEF
`; if (stats.critChance > 0) html += `
🎯 +${Math.round(stats.critChance * 100)}% Crit
`; if (stats.lifesteal > 0) html += `
💚 ${Math.round(stats.lifesteal * 100)}% Lifesteal
`; if (stats.attackSpeed !== 1.0) html += `
⚡ ${Math.round(stats.attackSpeed * 100)}% ATK Spd
`; if (stats.moveSpeed !== 1.0) html += `
👢 ${Math.round(stats.moveSpeed * 100)}% Move Spd
`; if (stats.lootBonus > 0) html += `
🍀 +${Math.round(stats.lootBonus * 100)}% Loot
`; if (stats.dodgeBonus > 0) html += `
💨 +${Math.round(stats.dodgeBonus * 100)}% Dodge
`; statsEl.innerHTML = html; } } // v5.1: Enchantment System - v6.68: Massively expanded with new enchantments const ENCHANTMENTS = { // Original enchantments sharpness: { name: 'Sharpness', icon: '🔪', stat: 'damage', bonus: 3, slots: ['weapon'], cost: { 'Enchant Shard': 2, 'Arcane Dust': 5 } }, fortify: { name: 'Fortify', icon: '🏰', stat: 'defense', bonus: 2, slots: ['armor'], cost: { 'Enchant Shard': 2, 'Arcane Dust': 5 } }, swiftness: { name: 'Swiftness', icon: '💨', stat: 'moveSpeed', bonus: 0.05, slots: ['accessory'], cost: { 'Enchant Shard': 1, 'Arcane Dust': 3 }, multiplicative: true }, luck: { name: 'Luck', icon: '🍀', stat: 'lootBonus', bonus: 0.05, slots: ['accessory'], cost: { 'Enchant Shard': 3, 'Arcane Dust': 8 } }, efficiency: { name: 'Efficiency', icon: '⚡', stat: 'miningBonus', bonus: 1, slots: ['tool'], cost: { 'Enchant Shard': 2, 'Arcane Dust': 4 } }, lure: { name: 'Lure', icon: '🎣', stat: 'fishingBonus', bonus: 1, slots: ['tool'], cost: { 'Enchant Shard': 2, 'Arcane Dust': 4 } }, critical: { name: 'Critical', icon: '🎯', stat: 'critChance', bonus: 0.05, slots: ['weapon', 'accessory'], cost: { 'Enchant Shard': 4, 'Arcane Dust': 10 } }, vampiric: { name: 'Vampiric', icon: '🦇', stat: 'lifesteal', bonus: 0.05, slots: ['weapon'], cost: { 'Enchant Shard': 5, 'Arcane Dust': 15 } }, // v6.68: NEW ENCHANTMENTS elemental_fury: { name: 'Elemental Fury', icon: '🌀', stat: 'elementalDamage', bonus: 0.15, slots: ['weapon'], cost: { 'Enchant Shard': 4, 'Arcane Dust': 12 } }, executioner: { name: 'Executioner', icon: '💀', stat: 'executeDamage', bonus: 0.20, slots: ['weapon'], cost: { 'Enchant Shard': 5, 'Arcane Dust': 15 } }, protection: { name: 'Protection', icon: '🛡️', stat: 'defense', bonus: 5, slots: ['armor'], cost: { 'Enchant Shard': 3, 'Arcane Dust': 8 } }, vitality: { name: 'Vitality', icon: '❤️', stat: 'maxHp', bonus: 20, slots: ['armor'], cost: { 'Enchant Shard': 3, 'Arcane Dust': 10 } }, regeneration: { name: 'Regeneration', icon: '💚', stat: 'hpRegen', bonus: 0.02, slots: ['armor'], cost: { 'Enchant Shard': 4, 'Arcane Dust': 12 }, multiplicative: true }, thorns: { name: 'Thorns', icon: '🌵', stat: 'thornsDamage', bonus: 0.05, slots: ['armor'], cost: { 'Enchant Shard': 3, 'Arcane Dust': 10 }, multiplicative: true }, haste: { name: 'Haste', icon: '⚡', stat: 'attackSpeed', bonus: 0.05, slots: ['accessory', 'weapon'], cost: { 'Enchant Shard': 3, 'Arcane Dust': 8 }, multiplicative: true }, fortune: { name: 'Fortune', icon: '💰', stat: 'lootBonus', bonus: 0.10, slots: ['accessory', 'tool'], cost: { 'Enchant Shard': 4, 'Arcane Dust': 12 }, multiplicative: true }, wisdom: { name: 'Wisdom', icon: '📚', stat: 'xpBonus', bonus: 0.10, slots: ['accessory'], cost: { 'Enchant Shard': 3, 'Arcane Dust': 10 }, multiplicative: true }, silk_touch: { name: 'Silk Touch', icon: '🎀', stat: 'bonusRare', bonus: 0.10, slots: ['tool'], cost: { 'Enchant Shard': 5, 'Arcane Dust': 20 }, multiplicative: true }, unbreaking: { name: 'Unbreaking', icon: '💎', stat: 'durability', bonus: 1, slots: ['weapon', 'armor', 'tool', 'accessory'], cost: { 'Enchant Shard': 6, 'Arcane Dust': 25 } } }; // v5.1: Get enchantments for an item function getItemEnchantments(itemName) { if (!gameData.enchantments) gameData.enchantments = {}; return gameData.enchantments[itemName] || []; } // v5.1: Check if enchantment can be applied function canEnchant(itemName, enchantId) { const equipData = EQUIPMENT_MAP[itemName]; if (!equipData) return false; const enchant = ENCHANTMENTS[enchantId]; if (!enchant) return false; // Check slot compatibility if (!enchant.slots.includes(equipData.slot)) return false; // Check if already has this enchantment const currentEnchants = getItemEnchantments(itemName); if (currentEnchants.includes(enchantId)) return false; // Check max enchantments (3 per item) if (currentEnchants.length >= 3) return false; // Check materials for (const [mat, count] of Object.entries(enchant.cost)) { if (!hasItem(mat, count)) return false; } return true; } // v5.1: Apply enchantment to item function applyEnchantment(itemName, enchantId) { if (!canEnchant(itemName, enchantId)) { showNotification('Cannot apply this enchantment!', 'error'); return false; } const enchant = ENCHANTMENTS[enchantId]; // Consume materials for (const [mat, count] of Object.entries(enchant.cost)) { removeItem(mat, count); } // Apply enchantment if (!gameData.enchantments) gameData.enchantments = {}; if (!gameData.enchantments[itemName]) gameData.enchantments[itemName] = []; gameData.enchantments[itemName].push(enchantId); showNotification(`Applied ${enchant.icon} ${enchant.name} to ${itemName}!`, 'success'); AudioSystem.levelUp(); saveGameData(); updateEnchantModal(); updateEquipmentUI(); return true; } // v5.1: Get total stats including enchantments function getEnchantmentBonuses(itemName) { const bonuses = {}; const enchants = getItemEnchantments(itemName); for (const enchantId of enchants) { const enchant = ENCHANTMENTS[enchantId]; if (!enchant) continue; if (enchant.multiplicative) { bonuses[enchant.stat] = (bonuses[enchant.stat] || 1) * (1 + enchant.bonus); } else { bonuses[enchant.stat] = (bonuses[enchant.stat] || 0) + enchant.bonus; } } return bonuses; } // v5.1: Show enchant modal // v8.24: Added null safety check for modal element function showEnchantModal() { const modal = document.getElementById('enchant-modal'); if (modal) modal.style.display = 'flex'; updateEnchantModal(); } function closeEnchantModal() { const modal = document.getElementById('enchant-modal'); if (modal) modal.style.display = 'none'; } function updateEnchantModal() { const gear = getEquippedGear(); const itemsDiv = document.getElementById('enchant-items'); const enchantsDiv = document.getElementById('enchant-options'); // List equipped items let itemsHtml = ''; for (const [slot, itemName] of Object.entries(gear)) { if (!itemName) continue; const itemDef = ITEMS[itemName]; const enchants = getItemEnchantments(itemName); const enchantIcons = enchants.map(e => ENCHANTMENTS[e]?.icon || '?').join(''); itemsHtml += `
${itemDef?.icon || '?'} ${itemName} ${enchantIcons || 'No enchants'}
`; } itemsDiv.innerHTML = itemsHtml || '
Equip items first!
'; // Default: no item selected enchantsDiv.innerHTML = '
Select an item to enchant
'; } let selectedEnchantItem = null; function selectEnchantItem(itemName) { selectedEnchantItem = itemName; const equipData = EQUIPMENT_MAP[itemName]; const enchantsDiv = document.getElementById('enchant-options'); // Highlight selected item document.querySelectorAll('.enchant-item').forEach(el => { el.style.background = el.dataset.item === itemName ? 'rgba(68, 136, 255, 0.2)' : ''; el.style.borderColor = el.dataset.item === itemName ? '#4af' : '#444'; }); // Show available enchantments let html = '
Available Enchantments:
'; for (const [id, enchant] of Object.entries(ENCHANTMENTS)) { if (!enchant.slots.includes(equipData.slot)) continue; const canApply = canEnchant(itemName, id); const hasIt = getItemEnchantments(itemName).includes(id); const costStr = Object.entries(enchant.cost).map(([m, c]) => `${c}x ${m}`).join(', '); html += `
${enchant.icon} ${enchant.name} ${hasIt ? '✓ Applied' : canApply ? `` : 'Need materials'}
+${enchant.bonus}${enchant.multiplicative ? '%' : ''} ${enchant.stat} | Cost: ${costStr}
`; } enchantsDiv.innerHTML = html; } // v5.2: Talent Tree System - v6.68: MASSIVELY EXPANDED with 6 trees const TALENT_TREES = { combat: { name: 'Combat Mastery', icon: '⚔️', color: '#ff4444', talents: { brutality: { name: 'Brutality', desc: '+5% damage per rank', maxRank: 5, effect: { damage: 0.05 } }, toughness: { name: 'Toughness', desc: '+10 max HP per rank', maxRank: 5, effect: { maxHp: 10 } }, precision: { name: 'Precision', desc: '+2% crit chance per rank', maxRank: 5, effect: { critChance: 0.02 }, requires: 'brutality' }, bloodlust: { name: 'Bloodlust', desc: '+1% lifesteal per rank', maxRank: 3, effect: { lifesteal: 0.01 }, requires: 'precision' }, warlord: { name: 'Warlord', desc: '+10% ability damage', maxRank: 1, effect: { abilityDamage: 0.10 }, requires: 'bloodlust' }, // v6.68: New combat talents berserker_rage: { name: 'Berserker Rage', desc: '+15% damage when below 30% HP', maxRank: 3, effect: { lowHpDamage: 0.15 }, requires: 'brutality' }, armor_crush: { name: 'Armor Crush', desc: 'Attacks reduce enemy defense by 2', maxRank: 3, effect: { armorPen: 2 }, requires: 'precision' }, executioner: { name: 'Executioner', desc: '+25% damage to enemies below 25% HP', maxRank: 2, effect: { executeDamage: 0.25 }, requires: 'warlord' }, war_cry: { name: 'War Cry', desc: 'Abilities buff allies +10% damage', maxRank: 1, effect: { allyDamageBuff: 0.10 }, requires: 'executioner' } } }, survival: { name: 'Survival Instinct', icon: '🛡️', color: '#44aaff', talents: { thick_skin: { name: 'Thick Skin', desc: '+2 defense per rank', maxRank: 5, effect: { defense: 2 } }, evasion: { name: 'Evasion', desc: '+3% dodge chance per rank', maxRank: 5, effect: { dodgeChance: 0.03 } }, second_wind: { name: 'Second Wind', desc: '+5% HP regen per rank', maxRank: 3, effect: { hpRegen: 0.05 }, requires: 'thick_skin' }, fortress: { name: 'Fortress', desc: '+20% shield duration', maxRank: 3, effect: { shieldDuration: 0.20 }, requires: 'evasion' }, immortal: { name: 'Immortal', desc: 'Survive fatal blow once/world', maxRank: 1, effect: { deathSave: true }, requires: 'second_wind' }, // v6.68: New survival talents iron_will: { name: 'Iron Will', desc: 'Reduce CC duration by 10%', maxRank: 3, effect: { ccReduction: 0.10 }, requires: 'thick_skin' }, thorns: { name: 'Thorns', desc: 'Reflect 5% damage back to attackers', maxRank: 3, effect: { thornsDamage: 0.05 }, requires: 'fortress' }, last_stand: { name: 'Last Stand', desc: '+30% defense when below 25% HP', maxRank: 2, effect: { lowHpDefense: 0.30 }, requires: 'immortal' }, phoenix_spirit: { name: 'Phoenix Spirit', desc: 'Revive with 50% HP once per planet', maxRank: 1, effect: { autoRevive: true }, requires: 'last_stand' } } }, fortune: { name: 'Fortune Seeker', icon: '🍀', color: '#44ff44', talents: { lucky: { name: 'Lucky', desc: '+3% loot drop per rank', maxRank: 5, effect: { lootBonus: 0.03 } }, harvester: { name: 'Harvester', desc: '+10% resource yield per rank', maxRank: 5, effect: { resourceYield: 0.10 } }, treasure_sense: { name: 'Treasure Sense', desc: '+5% rare find per rank', maxRank: 3, effect: { rareFind: 0.05 }, requires: 'lucky' }, midas_touch: { name: 'Midas Touch', desc: '+15% XP gain per rank', maxRank: 3, effect: { xpBonus: 0.15 }, requires: 'harvester' }, jackpot: { name: 'Jackpot', desc: 'Double boss loot chance', maxRank: 1, effect: { doubleBossLoot: true }, requires: 'treasure_sense' }, // v6.68: New fortune talents scavenger: { name: 'Scavenger', desc: '+1 extra item from resource nodes', maxRank: 2, effect: { extraResource: 1 }, requires: 'harvester' }, golden_touch: { name: 'Golden Touch', desc: '+20% chance for items to upgrade rarity', maxRank: 3, effect: { rarityUpgrade: 0.20 }, requires: 'treasure_sense' }, hoarder: { name: 'Hoarder', desc: '+5 inventory slots per rank', maxRank: 2, effect: { inventorySlots: 5 }, requires: 'midas_touch' }, legendary_luck: { name: 'Legendary Luck', desc: '5% chance for any drop to be Legendary', maxRank: 1, effect: { legendaryChance: 0.05 }, requires: 'jackpot' } } }, // v6.68: NEW TREE - Arcane Mastery arcane: { name: 'Arcane Mastery', icon: '🔮', color: '#aa44ff', talents: { mana_pool: { name: 'Mana Pool', desc: '+10% max mana per rank', maxRank: 5, effect: { maxMana: 0.10 } }, spell_power: { name: 'Spell Power', desc: '+8% ability damage per rank', maxRank: 5, effect: { spellDamage: 0.08 } }, arcane_mastery: { name: 'Arcane Mastery', desc: '-5% ability cooldowns per rank', maxRank: 5, effect: { cooldownReduction: 0.05 }, requires: 'mana_pool' }, elemental_attunement: { name: 'Elemental Attunement', desc: '+15% elemental damage', maxRank: 3, effect: { elementalDamage: 0.15 }, requires: 'spell_power' }, mystic_barrier: { name: 'Mystic Barrier', desc: 'Abilities grant 5% max HP shield', maxRank: 3, effect: { abilityShield: 0.05 }, requires: 'arcane_mastery' }, spell_echo: { name: 'Spell Echo', desc: '15% chance abilities trigger twice', maxRank: 2, effect: { spellEcho: 0.15 }, requires: 'elemental_attunement' }, archmage: { name: 'Archmage', desc: 'Abilities cost 30% less mana', maxRank: 1, effect: { manaCostReduction: 0.30 }, requires: 'spell_echo' } } }, // v6.68: NEW TREE - Velocity velocity: { name: 'Velocity', icon: '💨', color: '#ffaa00', talents: { quick_feet: { name: 'Quick Feet', desc: '+3% movement speed per rank', maxRank: 5, effect: { moveSpeed: 0.03 } }, attack_speed: { name: 'Attack Speed', desc: '+5% attack speed per rank', maxRank: 5, effect: { attackSpeed: 0.05 } }, momentum: { name: 'Momentum', desc: '+2% damage per second moving', maxRank: 3, effect: { momentumDamage: 0.02 }, requires: 'quick_feet' }, lightning_reflexes: { name: 'Lightning Reflexes', desc: '+5% dodge while moving', maxRank: 3, effect: { movingDodge: 0.05 }, requires: 'attack_speed' }, blitz: { name: 'Blitz', desc: 'First attack after moving deals +20% damage', maxRank: 2, effect: { blitzDamage: 0.20 }, requires: 'momentum' }, afterimage: { name: 'Afterimage', desc: '10% chance to leave damaging clone', maxRank: 2, effect: { afterimageChance: 0.10 }, requires: 'lightning_reflexes' }, time_dilation: { name: 'Time Dilation', desc: 'Slow nearby enemies by 15%', maxRank: 1, effect: { aoeSlowAura: 0.15 }, requires: 'blitz' } } }, // v6.68: NEW TREE - Crafting Mastery crafting: { name: 'Crafting Mastery', icon: '🔨', color: '#ff8844', talents: { efficient_crafting: { name: 'Efficient Crafting', desc: '-5% material cost per rank', maxRank: 5, effect: { materialCost: 0.05 } }, quality_work: { name: 'Quality Work', desc: '+10% crafted item stats per rank', maxRank: 5, effect: { craftedStats: 0.10 } }, salvage_expert: { name: 'Salvage Expert', desc: '+20% materials from dismantling', maxRank: 3, effect: { salvageYield: 0.20 }, requires: 'efficient_crafting' }, masterwork: { name: 'Masterwork', desc: '10% chance craft is auto-upgraded', maxRank: 3, effect: { masterworkChance: 0.10 }, requires: 'quality_work' }, enchanting_affinity: { name: 'Enchanting Affinity', desc: '+1 enchantment slot on crafted items', maxRank: 2, effect: { extraEnchantSlot: 1 }, requires: 'salvage_expert' }, legendary_smith: { name: 'Legendary Smith', desc: 'Can craft Legendary tier items', maxRank: 1, effect: { craftLegendary: true }, requires: 'masterwork' }, dual_craft: { name: 'Dual Craft', desc: '25% chance to craft two items', maxRank: 1, effect: { doubleCraft: 0.25 }, requires: 'legendary_smith' } } } }; // v6.68: ITEM RARITY SYSTEM - Colors and stat multipliers const ITEM_RARITIES = { common: { name: 'Common', color: '#aaaaaa', statMult: 1.0, dropWeight: 60 }, uncommon: { name: 'Uncommon', color: '#1eff00', statMult: 1.2, dropWeight: 25 }, rare: { name: 'Rare', color: '#0070dd', statMult: 1.5, dropWeight: 10 }, epic: { name: 'Epic', color: '#a335ee', statMult: 2.0, dropWeight: 4 }, legendary: { name: 'Legendary', color: '#ff8000', statMult: 3.0, dropWeight: 0.9 }, mythic: { name: 'Mythic', color: '#ff00ff', statMult: 5.0, dropWeight: 0.1 } }; // v6.68: ITEM SET SYSTEM - Equipping multiple set pieces grants bonuses const ITEM_SETS = { voidwalker: { name: 'Voidwalker Set', color: '#9900ff', pieces: ['Void Dagger', 'Void Cloak', 'Void Ring'], bonuses: { 2: { desc: '+15% Void damage', effect: { voidDamage: 0.15 } }, 3: { desc: '+30% Void damage, Phase through enemies', effect: { voidDamage: 0.30, phaseWalk: true } } } }, inferno: { name: 'Inferno Set', color: '#ff4400', pieces: ['Magma Sword', 'Inferno Plate', 'Flame Ring'], bonuses: { 2: { desc: '+20% Fire damage, Burn on hit', effect: { fireDamage: 0.20, burnOnHit: true } }, 3: { desc: 'Fire nova every 5 kills', effect: { fireNova: 5 } } } }, frost: { name: 'Frostborne Set', color: '#00ccff', pieces: ['Frost Blade', 'Frost Armor', 'Frost Heart'], bonuses: { 2: { desc: '+15% Ice damage, Slow on hit', effect: { iceDamage: 0.15, slowOnHit: 0.20 } }, 3: { desc: 'Freeze enemies at low HP', effect: { freezeExecute: true } } } }, berserker: { name: 'Berserker Set', color: '#cc0000', pieces: ['Berserker Badge', 'Berserker Helm', 'Berserker Gauntlets'], bonuses: { 2: { desc: '+20% Attack Speed, +10% Damage', effect: { attackSpeed: 0.20, damage: 0.10 } }, 3: { desc: 'Gain Frenzy on kill (+50% speed for 3s)', effect: { frenzyOnKill: true } } } }, guardian: { name: 'Guardian Set', color: '#4488ff', pieces: ['Guardian Armor', 'Guardian Shield', 'Guardian Helm'], bonuses: { 2: { desc: '+30 Defense, +50 Max HP', effect: { defense: 30, maxHp: 50 } }, 3: { desc: 'Immunity for 2s when hit below 20% HP', effect: { lowHpImmunity: true } } } }, harvester: { name: 'Harvester Set', color: '#00ff88', pieces: ['Crystal Pickaxe', 'Harvester Vest', 'Master Rod'], bonuses: { 2: { desc: '+50% Resource yield', effect: { resourceYield: 0.50 } }, 3: { desc: 'Double XP from gathering', effect: { gatherXpMult: 2.0 } } } }, celestial: { name: 'Celestial Set', color: '#ffdd00', pieces: ['Legendary Blade', 'Celestial Armor', 'Mythic Orb'], bonuses: { 2: { desc: '+25% All damage, +25% Defense', effect: { damage: 0.25, defense: 25 } }, 3: { desc: 'Summon star guardian on ability use', effect: { starGuardian: true } } } } }; // v6.68: Get active set bonuses function getActiveSetBonuses() { const equippedItems = []; if (gameData.equipment) { Object.values(gameData.equipment).forEach(item => { if (item && item.name) equippedItems.push(item.name); }); } const bonuses = { effects: {}, activesets: [] }; for (const [setId, setData] of Object.entries(ITEM_SETS)) { const piecesEquipped = setData.pieces.filter(p => equippedItems.includes(p)).length; if (piecesEquipped >= 2) { bonuses.activesets.push({ name: setData.name, pieces: piecesEquipped, total: setData.pieces.length }); // Apply bonuses for each threshold reached for (const [threshold, bonus] of Object.entries(setData.bonuses)) { if (piecesEquipped >= parseInt(threshold)) { for (const [stat, value] of Object.entries(bonus.effect)) { if (typeof value === 'boolean') { bonuses.effects[stat] = value; } else { bonuses.effects[stat] = (bonuses.effects[stat] || 0) + value; } } } } } } return bonuses; } // v6.68: Enhanced enchantment tiers added to existing system via ENCHANTMENTS object // New enchantments: elemental_fury, executioner, protection, vitality, regeneration, thorns, haste, fortune, wisdom, silk_touch, unbreaking // These integrate with the existing v5.1 ENCHANTMENTS system above // v6.68: Generate random item rarity based on luck function rollItemRarity(baseLuck = 0) { const talentBonuses = getTalentBonuses(); const luck = baseLuck + (talentBonuses.rareFind || 0) + (talentBonuses.legendaryChance || 0); // Calculate drop chances let roll = Math.random() * 100; // Luck shifts the roll toward rarer items roll -= luck * 50; // Each 1% luck shifts 0.5% toward rare if (roll < ITEM_RARITIES.mythic.dropWeight) return 'mythic'; roll -= ITEM_RARITIES.mythic.dropWeight; if (roll < ITEM_RARITIES.legendary.dropWeight) return 'legendary'; roll -= ITEM_RARITIES.legendary.dropWeight; if (roll < ITEM_RARITIES.epic.dropWeight) return 'epic'; roll -= ITEM_RARITIES.epic.dropWeight; if (roll < ITEM_RARITIES.rare.dropWeight) return 'rare'; roll -= ITEM_RARITIES.rare.dropWeight; if (roll < ITEM_RARITIES.uncommon.dropWeight) return 'uncommon'; return 'common'; } // v6.68: Create item with rarity function createRarityItem(baseName, forcedRarity = null) { const rarity = forcedRarity || rollItemRarity(); const rarityData = ITEM_RARITIES[rarity]; const baseItem = ITEMS[baseName] || {}; const item = { name: baseName, rarity: rarity, rarityColor: rarityData.color, displayName: rarity === 'common' ? baseName : `${rarityData.name} ${baseName}`, statMultiplier: rarityData.statMult, ...baseItem }; // Apply stat multiplier to numeric bonuses if (baseItem.combatBonus) item.combatBonus = Math.round(baseItem.combatBonus * rarityData.statMult); if (baseItem.defenseBonus) item.defenseBonus = Math.round(baseItem.defenseBonus * rarityData.statMult); if (baseItem.miningBonus) item.miningBonus = Math.round(baseItem.miningBonus * rarityData.statMult); if (baseItem.heal) item.heal = Math.round(baseItem.heal * rarityData.statMult); return item; } // ============================================ // v6.68: LIVING ECONOMY SYSTEM // Dynamic marketplace with NPC traders, fluctuating prices, // supply/demand simulation, and market manipulation // ============================================ const ECONOMY = { // Base prices for all tradeable items (in gold) basePrices: { // Raw resources (cheap, high volume) 'Log': 5, 'Ore': 8, 'Slime': 3, 'Raw Fish': 4, 'Chitin': 12, 'Frost Shard': 25, 'Magma Gem': 30, 'Void Fragment': 45, 'Crystal': 35, 'Obsidian': 20, 'Elite Essence': 50, // Crafted consumables 'Cooked Fish': 12, 'Health Potion': 25, 'Super Potion': 60, // Gear (expensive) 'Pickaxe': 50, 'Sword': 80, 'Fishing Rod': 40, 'Frost Blade': 200, 'Magma Sword': 280, 'Void Dagger': 400, 'Crystal Pickaxe': 150, 'Iron Armor': 100, 'Steel Armor': 250, 'Chitin Armor': 180, 'Guardian Armor': 800, 'Legendary Blade': 2000, 'Berserker Badge': 600, 'Vampiric Fang': 750, // Rare materials 'Boss Trophy': 150, 'Legendary Core': 500, 'Ancient Artifact': 200, 'Mystic Orb': 120, 'Enchant Shard': 80, 'Arcane Dust': 15 }, // Current market state supply: {}, // How much of each item is in the market demand: {}, // How much NPCs want each item priceHistory: {}, // Track price changes over time lastUpdate: 0, updateInterval: 30000, // Update prices every 30 seconds volatility: 0.15, // Max price swing per update (15%) // Market events activeEvents: [], eventChance: 0.05, // 5% chance per update for market event // Player's gold gold: 500, // Starting gold totalEarned: 0, totalSpent: 0, // Trading stats tradeHistory: [], maxTradeHistory: 100 }; // NPC Merchants with unique personalities and specializations const MERCHANTS = { grimjaw: { name: 'Grimjaw the Scrapper', icon: '🦾', specialty: 'resources', greeting: "Got junk? I'll take it off your hands... for the right price.", buyMultiplier: 0.7, // Pays 70% of market price sellMultiplier: 1.1, // Sells at 110% of market price preferred: ['Ore', 'Log', 'Chitin', 'Slime'], despised: ['Crystal', 'Mystic Orb'], // Pays less for these mood: 'neutral', inventory: {}, gold: 2000, restockTime: 60000 }, crystalia: { name: 'Crystalia Gemweaver', icon: '💎', specialty: 'gems', greeting: "Such beautiful specimens you bring me... Let us discuss terms.", buyMultiplier: 0.85, sellMultiplier: 1.2, preferred: ['Crystal', 'Frost Shard', 'Magma Gem', 'Void Fragment', 'Mystic Orb'], despised: ['Log', 'Slime'], mood: 'neutral', inventory: {}, gold: 5000, restockTime: 90000 }, ironhide: { name: 'Ironhide the Armorer', icon: '🛡️', specialty: 'equipment', greeting: "Need protection? My wares have saved countless lives.", buyMultiplier: 0.6, // Doesn't want to buy gear back sellMultiplier: 1.3, // Premium prices for quality preferred: ['Iron Armor', 'Steel Armor', 'Chitin Armor', 'Guardian Armor'], despised: ['Raw Fish', 'Slime'], mood: 'neutral', inventory: {}, gold: 8000, restockTime: 120000 }, shadowmere: { name: 'Shadowmere the Fence', icon: '🦇', specialty: 'rare', greeting: "Psst... I deal in items others won't touch. No questions asked.", buyMultiplier: 0.9, // Best buy prices for rare stuff sellMultiplier: 1.5, // But sells at huge markup preferred: ['Boss Trophy', 'Legendary Core', 'Elite Essence', 'Ancient Artifact'], despised: ['Log', 'Ore'], mood: 'neutral', inventory: {}, gold: 15000, restockTime: 180000 }, wanderbot: { name: 'Wanderbot 3000', icon: '🤖', specialty: 'random', greeting: "BEEP BOOP. TRADING PROTOCOLS ENGAGED. PREPARE FOR COMMERCE.", buyMultiplier: 0.75, sellMultiplier: 1.0, // Fair prices but random inventory preferred: [], // No preferences - truly random despised: [], mood: 'neutral', inventory: {}, gold: 3000, restockTime: 45000 } }; // ============================================ // v6.83: NPC EPISODIC MEMORY SYSTEM // NPCs with TRUE episodic memory - remembering events // with emotional weight, temporal decay, and gossip // ============================================ const NPC_MEMORY_SYSTEM = { // Individual NPC memories (initialized from MERCHANTS) npcMemories: {}, // Gossip network - rumors floating between NPCs gossipPool: [], // Memory system config config: { maxMemoriesPerNPC: 30, memoryDecayRatePerDay: 0.03, emotionalDecayRatePerDay: 0.02, gossipPropagationInterval: 60000, // 1 minute real time consolidationThresholdDays: 7, maxUnconsolidated: 20 }, // Last update timestamps lastDecayUpdate: Date.now(), lastGossipUpdate: Date.now() }; // NPC personality templates (affects how they remember) const NPC_PERSONALITIES = { grimjaw: { grudgeHolder: 0.8, // Very grudge-holding forgiving: 0.2, // Not forgiving gossiper: 0.4, // Moderate gossiper suspicious: 0.6, // Quite suspicious dramatic: 0.5 // Average dramatic }, crystalia: { grudgeHolder: 0.5, forgiving: 0.5, gossiper: 0.7, // Loves to gossip suspicious: 0.3, dramatic: 0.8 // Very dramatic }, ironhide: { grudgeHolder: 0.9, // Holds grudges forever forgiving: 0.1, gossiper: 0.2, // Stoic, doesn't gossip suspicious: 0.4, dramatic: 0.3 // Not dramatic }, shadowmere: { grudgeHolder: 0.7, forgiving: 0.3, gossiper: 0.9, // Information broker - gossips a lot suspicious: 0.9, // Very suspicious dramatic: 0.6 }, wanderbot: { grudgeHolder: 0.3, // Robot doesn't hold grudges forgiving: 0.7, // Fairly forgiving gossiper: 0.5, suspicious: 0.2, // Trusting dramatic: 0.4 } }; // Initialize memory structures for all merchants function initializeNPCMemories() { for (const merchantId of Object.keys(MERCHANTS)) { if (!NPC_MEMORY_SYSTEM.npcMemories[merchantId]) { NPC_MEMORY_SYSTEM.npcMemories[merchantId] = { episodicMemories: [], relationship: { trust: 0.5, respect: 0.5, fear: 0.0, familiarity: 0.0, lastInteraction: null, totalInteractions: 0 }, personality: NPC_PERSONALITIES[merchantId] || { grudgeHolder: 0.5, forgiving: 0.5, gossiper: 0.5, suspicious: 0.5, dramatic: 0.5 } }; } } // v8.0: Using SafeJSON for NPC memories (8-Strategy Consensus Cycle 7) const data = SafeJSON.fromLocalStorage('leviathan_npc_memories', null); if (data) { // Merge saved memories with initialized structures for (const [npcId, npcData] of Object.entries(data.npcMemories || {})) { if (NPC_MEMORY_SYSTEM.npcMemories[npcId]) { NPC_MEMORY_SYSTEM.npcMemories[npcId].episodicMemories = npcData.episodicMemories || []; NPC_MEMORY_SYSTEM.npcMemories[npcId].relationship = npcData.relationship || NPC_MEMORY_SYSTEM.npcMemories[npcId].relationship; } } NPC_MEMORY_SYSTEM.gossipPool = data.gossipPool || []; NPC_MEMORY_SYSTEM.lastDecayUpdate = data.lastDecayUpdate || Date.now(); NPC_MEMORY_SYSTEM.lastGossipUpdate = data.lastGossipUpdate || Date.now(); } } // Initialize NPC memories immediately after MERCHANTS is defined try { initializeNPCMemories(); } catch (e) { // v8.26: Enhanced error message with context console.error('[NPC Memory] v8.26: Failed to initialize NPC Memory System. This may affect merchant interactions. Error:', e.message || e); } // Save NPC memories to localStorage // v7.23: Added error handling for quota exceeded / private browsing function saveNPCMemories() { try { const data = { npcMemories: NPC_MEMORY_SYSTEM.npcMemories, gossipPool: NPC_MEMORY_SYSTEM.gossipPool, lastDecayUpdate: NPC_MEMORY_SYSTEM.lastDecayUpdate, lastGossipUpdate: NPC_MEMORY_SYSTEM.lastGossipUpdate }; localStorage.setItem('leviathan_npc_memories', JSON.stringify(data)); return true; } catch (e) { console.warn('[NPC Memory] Save failed:', e.message); if (e.name === 'QuotaExceededError') { if (typeof showNotification === 'function') { showNotification('Storage full - NPC memories may not persist', 'warning'); } } return false; } } // Load NPC memories from localStorage // v8.0: Using SafeJSON for NPC memories function (8-Strategy Consensus Cycle 8) function loadNPCMemories() { const data = SafeJSON.fromLocalStorage('leviathan_npc_memories', null); if (data) { if (data.npcMemories) { // Merge saved memories with existing NPC data (preserving personality) for (const [npcId, savedNpcData] of Object.entries(data.npcMemories)) { if (NPC_MEMORY_SYSTEM.npcMemories[npcId]) { NPC_MEMORY_SYSTEM.npcMemories[npcId].episodicMemories = savedNpcData.episodicMemories || []; NPC_MEMORY_SYSTEM.npcMemories[npcId].relationship = savedNpcData.relationship || NPC_MEMORY_SYSTEM.npcMemories[npcId].relationship; } } } if (data.gossipPool) { NPC_MEMORY_SYSTEM.gossipPool = data.gossipPool; } if (data.lastDecayUpdate) { NPC_MEMORY_SYSTEM.lastDecayUpdate = data.lastDecayUpdate; } if (data.lastGossipUpdate) { NPC_MEMORY_SYSTEM.lastGossipUpdate = data.lastGossipUpdate; } console.log('[NPC Memory] Loaded memories from save'); } } // Record a new memory for an NPC function recordNPCMemory(npcId, memoryData) { const npcData = NPC_MEMORY_SYSTEM.npcMemories[npcId]; if (!npcData) return; const memory = { id: 'mem_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9), type: memoryData.type, timestamp: Date.now(), gameTime: { day: gameData.stats?.daysPlayed || 0, phase: typeof DayNightCycle !== 'undefined' ? DayNightCycle.getCurrentPhase().name : 'day' }, event: memoryData.event, emotion: { primary: memoryData.emotion?.primary || 'neutral', intensity: memoryData.emotion?.intensity || 0.5, valence: memoryData.emotion?.valence || 0 }, fidelity: { details: 1.0, emotional: 1.0, source: memoryData.source || 'direct' }, recallCount: 0, lastRecall: null, consolidated: false }; npcData.episodicMemories.push(memory); // Update relationship based on emotional valence updateNPCRelationship(npcId, memory.emotion); // Limit memories if (npcData.episodicMemories.length > NPC_MEMORY_SYSTEM.config.maxMemoriesPerNPC) { // Remove oldest low-intensity memories npcData.episodicMemories.sort((a, b) => { const aScore = a.emotion.intensity + (a.consolidated ? 0.5 : 0); const bScore = b.emotion.intensity + (b.consolidated ? 0.5 : 0); return bScore - aScore; }); npcData.episodicMemories = npcData.episodicMemories.slice(0, NPC_MEMORY_SYSTEM.config.maxMemoriesPerNPC); } saveNPCMemories(); return memory; } // Calculate trade emotion based on item, quantity, and merchant preferences function calculateTradeEmotion(merchantId, item, quantity, price) { const merchant = MERCHANTS[merchantId]; if (!merchant) return { primary: 'neutral', intensity: 0.3, valence: 0 }; let emotion = { primary: 'satisfaction', intensity: 0.4, valence: 0.2 }; // Preferred items make them happy if (merchant.preferred.includes(item)) { emotion = { primary: 'gratitude', intensity: 0.7, valence: 0.6 }; if (quantity >= 10) emotion.intensity = 0.85; } // Despised items annoy them else if (merchant.despised.includes(item)) { emotion = { primary: 'annoyance', intensity: 0.5, valence: -0.3 }; } // Big trades are memorable if (price > 1000) { emotion.intensity = Math.min(1.0, emotion.intensity + 0.2); } return emotion; } // Update NPC relationship based on emotional event function updateNPCRelationship(npcId, emotion) { const npcData = NPC_MEMORY_SYSTEM.npcMemories[npcId]; if (!npcData) return; const rel = npcData.relationship; const intensity = emotion.intensity; const valence = emotion.valence; // Trust changes with positive/negative interactions rel.trust = Math.max(-1, Math.min(1, rel.trust + valence * intensity * 0.1)); // Respect increases with positive interactions, decreases with disrespect if (emotion.primary === 'gratitude' || emotion.primary === 'respect') { rel.respect = Math.min(1, rel.respect + intensity * 0.05); } else if (emotion.primary === 'anger' || emotion.primary === 'betrayal') { rel.respect = Math.max(-1, rel.respect - intensity * 0.1); } // Fear from harm if (emotion.primary === 'fear' || emotion.primary === 'betrayal') { rel.fear = Math.min(1, rel.fear + intensity * 0.2); } // Familiarity always increases with interaction rel.familiarity = Math.min(1, rel.familiarity + 0.02); rel.lastInteraction = Date.now(); rel.totalInteractions++; } // Memory decay - run periodically function updateNPCMemoryDecay() { const now = Date.now(); const daysSinceLastUpdate = (now - NPC_MEMORY_SYSTEM.lastDecayUpdate) / (1000 * 60 * 60 * 24); if (daysSinceLastUpdate < 0.01) return; // At least ~15 minutes between decay updates for (const [npcId, npcData] of Object.entries(NPC_MEMORY_SYSTEM.npcMemories)) { const personality = npcData.personality; for (const memory of npcData.episodicMemories) { // Detail decay: fast for neutral, slow for emotional const detailDecayRate = NPC_MEMORY_SYSTEM.config.memoryDecayRatePerDay / (1 + memory.emotion.intensity * 2); memory.fidelity.details = Math.max(0.2, memory.fidelity.details - detailDecayRate * daysSinceLastUpdate); // Emotional decay: modified by personality const emotionalDecayRate = memory.emotion.valence > 0 ? NPC_MEMORY_SYSTEM.config.emotionalDecayRatePerDay * personality.forgiving : NPC_MEMORY_SYSTEM.config.emotionalDecayRatePerDay * (1 - personality.grudgeHolder); memory.emotion.intensity = Math.max(0.1, memory.emotion.intensity - emotionalDecayRate * daysSinceLastUpdate); // Rumor decay: faster than direct memories if (memory.fidelity.source === 'rumor') { memory.fidelity.details *= Math.pow(0.95, daysSinceLastUpdate); } } // Consolidation: old but emotional memories become permanent consolidateNPCMemories(npcId); } NPC_MEMORY_SYSTEM.lastDecayUpdate = now; saveNPCMemories(); } // Consolidate memories (keep important ones, compress details of old ones) function consolidateNPCMemories(npcId) { const npcData = NPC_MEMORY_SYSTEM.npcMemories[npcId]; if (!npcData) return; const unconsolidated = npcData.episodicMemories.filter(m => !m.consolidated); if (unconsolidated.length > NPC_MEMORY_SYSTEM.config.maxUnconsolidated) { // Keep most emotional, consolidate the rest unconsolidated.sort((a, b) => b.emotion.intensity - a.emotion.intensity); for (let i = NPC_MEMORY_SYSTEM.config.maxUnconsolidated; i < unconsolidated.length; i++) { unconsolidated[i].consolidated = true; unconsolidated[i].fidelity.details = 0.3; // Vague details } } } // Recall a memory (with potential misremembering) function recallNPCMemory(npcId, context) { context = context || 'greeting'; const npcData = NPC_MEMORY_SYSTEM.npcMemories[npcId]; if (!npcData || npcData.episodicMemories.length === 0) return null; const personality = npcData.personality; // Filter relevant memories for context let relevantMemories = npcData.episodicMemories.filter(function(m) { if (context === 'trading') return m.type === 'TRADE'; if (context === 'greeting') return m.emotion.intensity > 0.3; return true; }); if (relevantMemories.length === 0) return null; // Probability weighted by emotion intensity and recency const weights = relevantMemories.map(function(m) { const recency = 1 / (1 + (Date.now() - m.timestamp) / (1000 * 60 * 60 * 24 * 7)); return m.emotion.intensity * recency; }); const totalWeight = weights.reduce(function(a, b) { return a + b; }, 0); let random = Math.random() * totalWeight; let selectedMemory = null; for (let i = 0; i < relevantMemories.length; i++) { random -= weights[i]; if (random <= 0) { selectedMemory = relevantMemories[i]; break; } } if (!selectedMemory) return null; // MISREMEMBERING: Distort based on fidelity and personality const distortedMemory = distortNPCMemory(selectedMemory, personality); // Update recall metadata selectedMemory.recallCount++; selectedMemory.lastRecall = Date.now(); // Reconsolidation: recalled memories strengthen selectedMemory.emotion.intensity = Math.min(1.0, selectedMemory.emotion.intensity * 1.05); return distortedMemory; } // Distort a memory based on fidelity function distortNPCMemory(memory, personality) { const distorted = JSON.parse(JSON.stringify(memory)); // Deep copy const fidelity = memory.fidelity.details; const dramatic = personality.dramatic; // Quantity distortion: remembered bigger/smaller if (distorted.event.quantity) { const distortionFactor = 1 + (Math.random() - 0.5) * (1 - fidelity) * dramatic * 2; distorted.event.quantity = Math.max(1, Math.round(distorted.event.quantity * distortionFactor)); } // Value distortion if (distorted.event.value || distorted.event.price) { const key = distorted.event.value ? 'value' : 'price'; const distortionFactor = 1 + (Math.random() - 0.5) * (1 - fidelity) * dramatic * 2; distorted.event[key] = Math.max(1, Math.round(distorted.event[key] * distortionFactor)); } // Emotional amplification: dramatic personalities exaggerate distorted.emotion.intensity = Math.min(1.0, distorted.emotion.intensity * (1 + dramatic * 0.3)); // Valence shift: suspicious personalities remember things as worse if (personality.suspicious > 0.5) { distorted.emotion.valence = Math.max(-1, distorted.emotion.valence - 0.1 * (1 - fidelity)); } distorted._isDistorted = true; distorted._originalFidelity = fidelity; return distorted; } // Create gossip from an interesting event function createNPCGossip(npcId, content) { // Only create gossip for interesting events const GOSSIP_WORTHY = ['BIG_TRADE', 'BOSS_KILL', 'PLAYER_DEATH', 'RARE_ITEM', 'HARM']; if (GOSSIP_WORTHY.indexOf(content.type) === -1) return; const npc = NPC_MEMORY_SYSTEM.npcMemories[npcId]; if (!npc || Math.random() > npc.personality.gossiper) return; NPC_MEMORY_SYSTEM.gossipPool.push({ id: 'gossip_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9), origin: npcId, about: 'player', content: content, spread: [npcId], distortion: 0, timestamp: Date.now(), potency: 0.7 }); saveNPCMemories(); } // Propagate gossip between NPCs function propagateNPCGossip() { const now = Date.now(); if (now - NPC_MEMORY_SYSTEM.lastGossipUpdate < NPC_MEMORY_SYSTEM.config.gossipPropagationInterval) return; for (let g = 0; g < NPC_MEMORY_SYSTEM.gossipPool.length; g++) { const gossip = NPC_MEMORY_SYSTEM.gossipPool[g]; const spreadCopy = gossip.spread.slice(); // Each NPC who knows it might spread it for (let k = 0; k < spreadCopy.length; k++) { const knowerNpcId = spreadCopy[k]; const knower = NPC_MEMORY_SYSTEM.npcMemories[knowerNpcId]; if (!knower) continue; // Check if they will gossip if (Math.random() > knower.personality.gossiper * gossip.potency) continue; // Find a nearby NPC who does not know yet const recipients = Object.keys(NPC_MEMORY_SYSTEM.npcMemories) .filter(function(id) { return id !== knowerNpcId && gossip.spread.indexOf(id) === -1; }); if (recipients.length === 0) continue; const recipientId = recipients[Math.floor(Math.random() * recipients.length)]; // Spread with distortion gossip.spread.push(recipientId); gossip.distortion += 0.1 * knower.personality.dramatic; // Recipient gets a memory (marked as rumor) const distortedContent = distortGossipContent(gossip.content, gossip.distortion); recordNPCMemory(recipientId, { type: 'RUMOR', event: distortedContent, emotion: { primary: gossip.content.type === 'BIG_TRADE' ? 'curiosity' : gossip.content.type === 'HARM' ? 'suspicion' : 'interest', intensity: 0.3 * (1 - gossip.distortion), valence: 0 }, source: 'rumor' }); } // Gossip loses potency over time gossip.potency *= 0.95; } // Remove dead gossip NPC_MEMORY_SYSTEM.gossipPool = NPC_MEMORY_SYSTEM.gossipPool.filter(function(g) { return g.potency > 0.1; }); NPC_MEMORY_SYSTEM.lastGossipUpdate = now; saveNPCMemories(); } // Distort gossip content as it spreads function distortGossipContent(content, distortion) { const distorted = JSON.parse(JSON.stringify(content)); if (distorted.value) { // Values get exaggerated distorted.value = Math.round(distorted.value * (1 + distortion * 2)); } if (distorted.quantity) { distorted.quantity = Math.round(distorted.quantity * (1 + distortion * 1.5)); } distorted._distortion = distortion; return distorted; } // Function aliases for cleaner API usage const recordMemory = recordNPCMemory; const recallMemory = recallNPCMemory; const updateMemoryDecay = updateNPCMemoryDecay; const propagateGossip = propagateNPCGossip; const createGossip = createNPCGossip; // Update relationship with simple attribute change function updateRelationship(npcId, attribute, delta) { const npcData = NPC_MEMORY_SYSTEM.npcMemories[npcId]; if (!npcData) return; if (typeof npcData.relationship[attribute] === 'number') { npcData.relationship[attribute] = Math.max(-1, Math.min(1, npcData.relationship[attribute] + delta)); } } // ============================================ // MEMORY DIALOGUE GENERATION SYSTEM // ============================================ const MEMORY_DIALOGUE = { // Greeting based on relationship and recent memories generateGreeting(npcId) { const npcData = NPC_MEMORY_SYSTEM.npcMemories[npcId]; if (!npcData) return MERCHANTS[npcId]?.greeting || "Welcome, traveler."; const merchant = MERCHANTS[npcId]; const rel = npcData.relationship; // Check for dominant recent memory const recentMemory = recallMemory(npcId, 'greeting'); // Build greeting layers let greeting = ''; // Familiarity layer if (rel.familiarity < 0.2) { greeting = merchant?.greeting || "Welcome, stranger."; } else if (rel.familiarity > 0.7) { greeting = "Ah, you again! "; } else { greeting = "Welcome back. "; } // Memory layer if (recentMemory) { greeting += this.generateMemoryReference(recentMemory, npcData.personality); } // Trust layer if (rel.trust < -0.3) { greeting += " I've got my eye on you."; } else if (rel.trust > 0.5) { greeting += " You're always welcome here."; } return greeting; }, generateMemoryReference(memory, personality) { const fidelity = memory.fidelity.details; const eventItem = memory.event.item || 'goods'; const eventQuantity = memory.event.quantity || 'some'; const eventEnemy = memory.event.enemy || 'creature'; const eventDays = memory.event.days || 'some'; const templates = { TRADE: { positive: [ fidelity > 0.7 ? 'Remember when you sold me ' + eventQuantity + ' ' + eventItem + '? Good times.' : fidelity > 0.4 ? 'Didn\'t you sell me some... ' + eventItem + ', was it?' : "We've done good business before, haven't we?" ], negative: [ fidelity > 0.7 ? 'Still waiting for a fair deal after that ' + eventItem + ' debacle.' : "Hmph. Your trades haven't always been to my liking." ] }, HELP: { positive: [ fidelity > 0.5 ? "I haven't forgotten what you did for me." : "I remember... you helped me once. Or was it someone else?" ], negative: [] }, HARM: { positive: [], negative: [ fidelity > 0.7 ? "You think I've forgotten? I NEVER forget." : "Something about you... sets my teeth on edge." ] }, WITNESS: { positive: [ 'I saw you take down that ' + eventEnemy + '. Impressive.' ], negative: [ 'I heard about your... encounter with that ' + eventEnemy + '.' ] }, RUMOR: { positive: [ "Word travels, you know. They say you're making waves." ], negative: [ "I've heard... things. About you." ] }, ABSENCE: { positive: [ eventDays > 14 ? eventDays + ' days! I thought you\'d vanished into the void.' : 'Been a while. ' + eventDays + ' days, give or take.' ], negative: [] } }; const valence = memory.emotion.valence > 0 ? 'positive' : 'negative'; const options = templates[memory.type]?.[valence] || []; if (options.length === 0) return ''; return options[Math.floor(Math.random() * options.length)]; }, // Unprompted memory surfacing (random chance during interaction) triggerUnpromptedMemory(npcId) { if (Math.random() > 0.15) return null; // 15% chance const npcData = NPC_MEMORY_SYSTEM.npcMemories[npcId]; if (!npcData) return null; // Find old negative memories (grudges) const grudge = npcData.episodicMemories.find(m => m.emotion.valence < -0.3 && m.emotion.intensity > 0.4 && (Date.now() - m.timestamp) > 7 * 24 * 60 * 60 * 1000 // At least a week old ); if (grudge) { const grudgeType = grudge.type === 'HARM' ? 'what you did' : 'that time'; const clarity = grudge.fidelity.details < 0.5 ? "The details are fuzzy, but the feeling isn't." : "I remember it clearly."; return { type: 'grudge', text: 'You know... I still think about ' + grudgeType + '. ' + clarity }; } // Find positive memories for warmth const warmMemory = npcData.episodicMemories.find(m => m.emotion.valence > 0.5 && m.emotion.intensity > 0.5 ); if (warmMemory && Math.random() > 0.7) { return { type: 'warmth', text: "You know, it's good to see a familiar face around here." }; } return null; }, // Generate trade-specific dialogue based on memory generateTradeComment(npcId, item, isBuying) { const npcData = NPC_MEMORY_SYSTEM.npcMemories[npcId]; if (!npcData) return null; // Find previous trades of this item const previousTrades = npcData.episodicMemories.filter(m => m.type === 'TRADE' && m.event.item === item ); if (previousTrades.length === 0) return null; const totalQuantity = previousTrades.reduce((sum, m) => sum + (m.event.quantity || 0), 0); if (totalQuantity > 100) { return 'Ah, ' + item + '. You\'ve brought me quite a lot of this over time.'; } else if (previousTrades.length > 3) { return item + ' again? You seem to have a steady supply.'; } return null; } }; // ============================================ // Market events that shake up the economy const MARKET_EVENTS = { ore_rush: { name: '⛏️ Ore Rush!', description: 'A new vein discovered! Ore prices crash as supply floods in.', duration: 120000, effects: { 'Ore': -0.5, 'Crystal': -0.3, 'Obsidian': -0.4 }, announcement: '📢 MARKET ALERT: Massive ore deposits discovered! Prices plummeting!' }, monster_surge: { name: '👹 Monster Surge!', description: 'Monster materials in high demand for defense contracts.', duration: 90000, effects: { 'Chitin': 0.8, 'Slime': 0.5, 'Elite Essence': 1.0, 'Boss Trophy': 0.6 }, announcement: '📢 MARKET ALERT: Military contracts driving monster material prices UP!' }, crystal_shortage: { name: '💎 Crystal Shortage!', description: 'Crystal mines collapsed! Prices skyrocket.', duration: 150000, effects: { 'Crystal': 1.5, 'Frost Shard': 0.8, 'Mystic Orb': 1.0 }, announcement: '📢 MARKET ALERT: Crystal shortage! Gem prices through the roof!' }, merchant_war: { name: '⚔️ Merchant War!', description: 'Merchants undercutting each other! Everything cheap!', duration: 60000, effects: { 'ALL': -0.3 }, // 30% off everything announcement: '📢 MARKET ALERT: Price war between merchants! BUY NOW!' }, luxury_boom: { name: '👑 Luxury Boom!', description: 'Nobles buying up all the fancy gear.', duration: 100000, effects: { 'Legendary Blade': 0.7, 'Guardian Armor': 0.6, 'Void Dagger': 0.5, 'Berserker Badge': 0.4 }, announcement: '📢 MARKET ALERT: Noble spending spree! Premium items in demand!' }, potion_plague: { name: '🧪 Plague Outbreak!', description: 'Sickness spreading! Healing items worth their weight in gold.', duration: 80000, effects: { 'Health Potion': 2.0, 'Super Potion': 1.8, 'Cooked Fish': 0.5, 'Slime': 0.6 }, announcement: '📢 MARKET ALERT: Plague spreading! Healing supplies critical!' }, tech_revolution: { name: '🔧 Tech Revolution!', description: 'New inventions drive demand for crafting materials.', duration: 110000, effects: { 'Ore': 0.4, 'Crystal': 0.3, 'Enchant Shard': 0.8, 'Arcane Dust': 0.5 }, announcement: '📢 MARKET ALERT: Inventors hoarding materials! Crafting supplies up!' }, black_market_bust: { name: '🚨 Black Market Bust!', description: 'Authorities cracked down! Rare items now scarce.', duration: 130000, effects: { 'Boss Trophy': 1.2, 'Legendary Core': 1.5, 'Elite Essence': 0.8 }, announcement: '📢 MARKET ALERT: Underground market raided! Rare goods prices surge!' } }; // Initialize economy state function initEconomy() { // Initialize supply/demand for all items for (const [item, basePrice] of Object.entries(ECONOMY.basePrices)) { ECONOMY.supply[item] = 50 + Math.random() * 50; // 50-100 initial supply ECONOMY.demand[item] = 50 + Math.random() * 50; // 50-100 initial demand ECONOMY.priceHistory[item] = [basePrice]; // Start with base price } // Initialize merchant inventories for (const [id, merchant] of Object.entries(MERCHANTS)) { restockMerchant(id); } // Load saved economy data // v8.0: Using SafeJSON for economy state (8-Strategy Consensus Cycle 4) const economyData = SafeJSON.fromLocalStorage('levi_economy', null); if (economyData) { if (economyData.gold !== undefined) ECONOMY.gold = economyData.gold; if (economyData.supply) ECONOMY.supply = { ...ECONOMY.supply, ...economyData.supply }; if (economyData.demand) ECONOMY.demand = { ...ECONOMY.demand, ...economyData.demand }; if (economyData.priceHistory) ECONOMY.priceHistory = economyData.priceHistory; if (economyData.totalEarned) ECONOMY.totalEarned = economyData.totalEarned; if (economyData.totalSpent) ECONOMY.totalSpent = economyData.totalSpent; console.log('[Economy] Loaded saved economy state'); } } // Save economy state function saveEconomy() { const data = { gold: ECONOMY.gold, supply: ECONOMY.supply, demand: ECONOMY.demand, priceHistory: ECONOMY.priceHistory, totalEarned: ECONOMY.totalEarned, totalSpent: ECONOMY.totalSpent }; localStorage.setItem('levi_economy', JSON.stringify(data)); } // Get current market price for an item function getMarketPrice(itemName) { const basePrice = ECONOMY.basePrices[itemName]; if (!basePrice) return 0; const supply = ECONOMY.supply[itemName] || 50; const demand = ECONOMY.demand[itemName] || 50; // Price = base * (demand / supply) with bounds let supplyDemandRatio = demand / Math.max(supply, 1); supplyDemandRatio = Math.max(0.2, Math.min(5.0, supplyDemandRatio)); // 0.2x to 5x let price = basePrice * supplyDemandRatio; // Apply active market events for (const event of ECONOMY.activeEvents) { const eventData = MARKET_EVENTS[event.type]; if (eventData.effects['ALL']) { price *= (1 + eventData.effects['ALL']); } if (eventData.effects[itemName]) { price *= (1 + eventData.effects[itemName]); } } return Math.max(1, Math.round(price)); } // Get price trend (up, down, stable) function getPriceTrend(itemName) { const history = ECONOMY.priceHistory[itemName]; if (!history || history.length < 2) return 'stable'; const current = history[history.length - 1]; const previous = history[history.length - 2]; const change = (current - previous) / previous; if (change > 0.05) return 'up'; if (change < -0.05) return 'down'; return 'stable'; } // Get merchant's adjusted price for buying from player function getMerchantBuyPrice(merchantId, itemName) { const merchant = MERCHANTS[merchantId]; if (!merchant) return 0; let price = getMarketPrice(itemName); // Apply merchant's buy multiplier price *= merchant.buyMultiplier; // Bonus for preferred items if (merchant.preferred.includes(itemName)) { price *= 1.2; } // Penalty for despised items if (merchant.despised.includes(itemName)) { price *= 0.5; } // Mood affects prices if (merchant.mood === 'happy') price *= 1.1; if (merchant.mood === 'angry') price *= 0.8; return Math.max(1, Math.round(price)); } // Get merchant's adjusted price for selling to player function getMerchantSellPrice(merchantId, itemName) { const merchant = MERCHANTS[merchantId]; if (!merchant) return Infinity; let price = getMarketPrice(itemName); // Apply merchant's sell multiplier price *= merchant.sellMultiplier; // Discount for preferred items (they have more stock) if (merchant.preferred.includes(itemName)) { price *= 0.9; } // Mood affects prices if (merchant.mood === 'happy') price *= 0.95; if (merchant.mood === 'angry') price *= 1.15; return Math.max(1, Math.round(price)); } // Sell item to merchant function sellToMerchant(merchantId, itemName, quantity = 1) { const merchant = MERCHANTS[merchantId]; if (!merchant) return false; // Check player has item if (!hasItem(itemName, quantity)) { showNotification(`You don't have ${quantity}x ${itemName}!`, 'error'); return false; } // Check merchant has gold const pricePerUnit = getMerchantBuyPrice(merchantId, itemName); const totalPrice = pricePerUnit * quantity; if (merchant.gold < totalPrice) { showNotification(`${merchant.name} can't afford that!`, 'error'); return false; } // Execute trade removeFromInventory(itemName, quantity); ECONOMY.gold += totalPrice; ECONOMY.totalEarned += totalPrice; merchant.gold -= totalPrice; // Add to merchant's inventory merchant.inventory[itemName] = (merchant.inventory[itemName] || 0) + quantity; // Increase supply (more on market) ECONOMY.supply[itemName] = (ECONOMY.supply[itemName] || 50) + quantity * 2; // Log trade logTrade('sell', merchantId, itemName, quantity, totalPrice); // v6.83: Record trade memory if (typeof recordMemory === 'function') { const emotion = (typeof calculateTradeEmotion === 'function') ? calculateTradeEmotion(merchantId, itemName, quantity, totalPrice) : { primary: 'gratitude', intensity: 0.4, valence: 0.3 }; recordMemory(merchantId, { type: 'TRADE', event: { action: 'bought_from_player', item: itemName, quantity: quantity, value: totalPrice }, emotion: emotion }); // Big trades generate gossip if (totalPrice > 1000 && typeof createGossip === 'function') { createGossip(merchantId, 'player', { type: 'BIG_SPENDER', item: itemName, value: totalPrice }); } } // Update merchant mood if (merchant.preferred.includes(itemName)) { merchant.mood = 'happy'; showNotification(`${merchant.name}: "Excellent! Just what I needed!"`, 'success'); } AudioSystem.pickup(); showNotification(`Sold ${quantity}x ${itemName} for ${totalPrice}g`, 'success'); saveEconomy(); updateMarketUI(); return true; } // Buy item from merchant function buyFromMerchant(merchantId, itemName, quantity = 1) { const merchant = MERCHANTS[merchantId]; if (!merchant) return false; // Check merchant has item const merchantStock = merchant.inventory[itemName] || 0; if (merchantStock < quantity) { showNotification(`${merchant.name} doesn't have ${quantity}x ${itemName}!`, 'error'); return false; } // Check player has gold const pricePerUnit = getMerchantSellPrice(merchantId, itemName); const totalPrice = pricePerUnit * quantity; if (ECONOMY.gold < totalPrice) { showNotification(`You need ${totalPrice}g (have ${ECONOMY.gold}g)`, 'error'); return false; } // Check inventory space if (gameData.inventory.length >= 20) { showNotification('Inventory full!', 'error'); return false; } // Execute trade ECONOMY.gold -= totalPrice; ECONOMY.totalSpent += totalPrice; merchant.gold += totalPrice; merchant.inventory[itemName] -= quantity; // Add to player inventory addToInventory(itemName, quantity); // Decrease supply, increase demand ECONOMY.supply[itemName] = Math.max(1, (ECONOMY.supply[itemName] || 50) - quantity); ECONOMY.demand[itemName] = (ECONOMY.demand[itemName] || 50) + quantity; // Log trade logTrade('buy', merchantId, itemName, quantity, totalPrice); // v6.83: Record trade memory (selling to player) if (typeof recordMemory === 'function') { recordMemory(merchantId, { type: 'TRADE', event: { action: 'sold_to_player', item: itemName, quantity: quantity, value: totalPrice }, emotion: { primary: 'satisfaction', intensity: 0.3 + Math.min(0.3, totalPrice / 5000), valence: 0.2 } }); } AudioSystem.pickup(); showNotification(`Bought ${quantity}x ${itemName} for ${totalPrice}g`, 'success'); saveEconomy(); updateMarketUI(); return true; } // Log a trade for history function logTrade(type, merchantId, itemName, quantity, totalPrice) { const trade = { type, merchant: merchantId, item: itemName, quantity, price: totalPrice, unitPrice: Math.round(totalPrice / quantity), timestamp: Date.now() }; ECONOMY.tradeHistory.push(trade); if (ECONOMY.tradeHistory.length > ECONOMY.maxTradeHistory) { ECONOMY.tradeHistory.shift(); } } // Restock merchant inventory function restockMerchant(merchantId) { const merchant = MERCHANTS[merchantId]; if (!merchant) return; merchant.inventory = {}; // Add specialty items const itemPool = Object.keys(ECONOMY.basePrices); // Preferred items get more stock for (const item of merchant.preferred) { merchant.inventory[item] = 3 + Math.floor(Math.random() * 8); } // Random items based on specialty const randomCount = 3 + Math.floor(Math.random() * 5); for (let i = 0; i < randomCount; i++) { const item = itemPool[Math.floor(Math.random() * itemPool.length)]; if (!merchant.despised.includes(item)) { merchant.inventory[item] = (merchant.inventory[item] || 0) + 1 + Math.floor(Math.random() * 3); } } // Reset gold merchant.gold = merchant === MERCHANTS.shadowmere ? 15000 : merchant === MERCHANTS.ironhide ? 8000 : merchant === MERCHANTS.crystalia ? 5000 : 3000; } // Update economy simulation (called periodically) function updateEconomy(time) { if (time - ECONOMY.lastUpdate < ECONOMY.updateInterval) return; ECONOMY.lastUpdate = time; // Natural supply/demand drift for (const item of Object.keys(ECONOMY.basePrices)) { // Random walk for supply and demand ECONOMY.supply[item] += (Math.random() - 0.5) * 10; ECONOMY.demand[item] += (Math.random() - 0.5) * 10; // Clamp values ECONOMY.supply[item] = Math.max(5, Math.min(200, ECONOMY.supply[item])); ECONOMY.demand[item] = Math.max(5, Math.min(200, ECONOMY.demand[item])); // Update price history const currentPrice = getMarketPrice(item); ECONOMY.priceHistory[item].push(currentPrice); if (ECONOMY.priceHistory[item].length > 20) { ECONOMY.priceHistory[item].shift(); } } // NPC-to-NPC trading simulation simulateNPCTrading(); // Check for market events if (Math.random() < ECONOMY.eventChance) { triggerMarketEvent(); } // Update active events ECONOMY.activeEvents = ECONOMY.activeEvents.filter(event => { if (time > event.endTime) { showNotification(`📢 ${event.name} has ended. Prices normalizing.`, 'info'); return false; } return true; }); // Merchant mood decay for (const merchant of Object.values(MERCHANTS)) { if (Math.random() < 0.3) { merchant.mood = 'neutral'; } } saveEconomy(); } // Simulate NPC merchants trading with each other function simulateNPCTrading() { const merchantIds = Object.keys(MERCHANTS); // Each merchant tries to buy items they prefer from others for (const buyerId of merchantIds) { const buyer = MERCHANTS[buyerId]; for (const preferredItem of buyer.preferred) { // Find a seller who has this item for (const sellerId of merchantIds) { if (sellerId === buyerId) continue; const seller = MERCHANTS[sellerId]; const sellerStock = seller.inventory[preferredItem] || 0; if (sellerStock > 2 && buyer.gold > getMarketPrice(preferredItem) * 2) { // Execute NPC trade const quantity = Math.min(2, sellerStock - 1); const price = getMarketPrice(preferredItem) * quantity; seller.inventory[preferredItem] -= quantity; buyer.inventory[preferredItem] = (buyer.inventory[preferredItem] || 0) + quantity; seller.gold += price; buyer.gold -= price; // This affects market prices! ECONOMY.demand[preferredItem] += 1; } } } } } // Trigger a random market event function triggerMarketEvent() { const eventTypes = Object.keys(MARKET_EVENTS); const eventType = eventTypes[Math.floor(Math.random() * eventTypes.length)]; const eventData = MARKET_EVENTS[eventType]; // Check if event already active if (ECONOMY.activeEvents.some(e => e.type === eventType)) return; const event = { type: eventType, name: eventData.name, startTime: performance.now(), endTime: performance.now() + eventData.duration }; ECONOMY.activeEvents.push(event); showNotification(eventData.announcement, 'warning'); AudioSystem.discovery(); } // Flood the market with an item (player manipulation) function floodMarket(itemName, quantity) { if (!hasItem(itemName, quantity)) { showNotification(`You don't have ${quantity}x ${itemName}!`, 'error'); return false; } // Remove from inventory removeFromInventory(itemName, quantity); // Massively increase supply ECONOMY.supply[itemName] = (ECONOMY.supply[itemName] || 50) + quantity * 5; // Prices will crash! const newPrice = getMarketPrice(itemName); const basePrice = ECONOMY.basePrices[itemName]; const crashPercent = Math.round((1 - newPrice / basePrice) * 100); showNotification(`📉 MARKET FLOODED! ${itemName} prices crashed ${crashPercent}%!`, 'warning'); AudioSystem.achievement(); saveEconomy(); updateMarketUI(); return true; } // Create artificial scarcity function createScarcity(itemName) { // Buy up all stock from merchants let totalBought = 0; let totalSpent = 0; for (const [id, merchant] of Object.entries(MERCHANTS)) { const stock = merchant.inventory[itemName] || 0; if (stock > 0) { const price = getMerchantSellPrice(id, itemName) * stock; if (ECONOMY.gold >= price) { ECONOMY.gold -= price; totalSpent += price; totalBought += stock; merchant.inventory[itemName] = 0; addToInventory(itemName, stock); } } } if (totalBought > 0) { // Decrease supply dramatically ECONOMY.supply[itemName] = Math.max(1, (ECONOMY.supply[itemName] || 50) - totalBought * 3); ECONOMY.demand[itemName] += totalBought; const newPrice = getMarketPrice(itemName); const basePrice = ECONOMY.basePrices[itemName]; const increasePercent = Math.round((newPrice / basePrice - 1) * 100); showNotification(`📈 CORNERED MARKET! Bought ${totalBought}x ${itemName} for ${totalSpent}g. Prices up ${increasePercent}%!`, 'success'); AudioSystem.achievement(); saveEconomy(); updateMarketUI(); } else { showNotification('No stock available to buy!', 'error'); } return totalBought > 0; } // Open market UI // v7.72: Removed debug console.log statements function openMarketUI() { const modal = document.getElementById('market-modal'); if (modal) { modal.style.display = 'flex'; updateMarketUI(); } } // v7.22: Expose to window for inline onclick handler window.openMarketUI = openMarketUI; // Close market UI function closeMarketUI() { const modal = document.getElementById('market-modal'); if (modal) { modal.style.display = 'none'; } } // Update market UI display function updateMarketUI() { const pricesDiv = document.getElementById('market-prices'); const merchantsDiv = document.getElementById('market-merchants'); const eventsDiv = document.getElementById('market-events'); const goldDisplay = document.getElementById('market-gold'); if (goldDisplay) { goldDisplay.textContent = `💰 ${ECONOMY.gold.toLocaleString()}g`; } // Price list with trends if (pricesDiv) { let html = '
'; for (const [item, basePrice] of Object.entries(ECONOMY.basePrices)) { const currentPrice = getMarketPrice(item); const trend = getPriceTrend(item); const trendIcon = trend === 'up' ? '📈' : trend === 'down' ? '📉' : '➡️'; const trendColor = trend === 'up' ? '#4f4' : trend === 'down' ? '#f44' : '#888'; const itemDef = ITEMS[item] || {}; const percentChange = Math.round((currentPrice / basePrice - 1) * 100); const changeStr = percentChange >= 0 ? `+${percentChange}%` : `${percentChange}%`; html += `
${itemDef.icon || '📦'} ${item} ${currentPrice}g ${trendIcon} ${changeStr}
`; } html += '
'; pricesDiv.innerHTML = html; } // Active events if (eventsDiv) { if (ECONOMY.activeEvents.length === 0) { eventsDiv.innerHTML = '
No active market events
'; } else { let html = ''; for (const event of ECONOMY.activeEvents) { const eventData = MARKET_EVENTS[event.type]; const remaining = Math.max(0, Math.round((event.endTime - performance.now()) / 1000)); html += `
${eventData.name} ${eventData.description} ⏱️ ${remaining}s remaining
`; } eventsDiv.innerHTML = html; } } } // Select merchant for trading let selectedMerchant = null; function selectMerchant(merchantId) { selectedMerchant = merchantId; // v6.83: Update relationship and check for memories if (typeof updateRelationship === 'function') { updateRelationship(merchantId, 'familiarity', 0.02); } // Check for unprompted memory surfacing if (typeof MEMORY_DIALOGUE !== 'undefined') { const unprompted = MEMORY_DIALOGUE.triggerUnpromptedMemory(merchantId); if (unprompted) { const merchant = MERCHANTS[merchantId]; setTimeout(() => { showNotification(merchant.icon + ' ' + unprompted.text, unprompted.type === 'grudge' ? 'warning' : 'info'); }, 1500); } } updateMerchantTradeUI(); } function updateMerchantTradeUI() { const tradeDiv = document.getElementById('merchant-trade'); if (!tradeDiv || !selectedMerchant) return; const merchant = MERCHANTS[selectedMerchant]; if (!merchant) return; // v6.83: Use memory-based greeting if available const greeting = (typeof MEMORY_DIALOGUE !== 'undefined') ? MEMORY_DIALOGUE.generateGreeting(selectedMerchant) : merchant.greeting; // v6.83: Get relationship info for display const npcData = NPC_MEMORY_SYSTEM?.npcMemories?.[selectedMerchant]; const rel = npcData?.relationship; const relationshipHTML = rel ? `
${rel.trust > 0.3 ? '💚' : rel.trust < -0.3 ? '💔' : '💛'} Trust: ${Math.round(rel.trust * 100)}% | Familiarity: ${Math.round(rel.familiarity * 100)}%
` : ''; let html = `
${merchant.icon} ${merchant.name} 💰 ${merchant.gold.toLocaleString()}g
${relationshipHTML}

"${greeting}"

🛒 Buy from ${merchant.name.split(' ')[0]}

`; // Items merchant is selling for (const [item, qty] of Object.entries(merchant.inventory)) { if (qty <= 0) continue; const price = getMerchantSellPrice(selectedMerchant, item); const itemDef = ITEMS[item] || {}; const canAfford = ECONOMY.gold >= price; html += `
${itemDef.icon || '📦'} ${item} x${qty} ${price}g
`; } html += `

💰 Sell to ${merchant.name.split(' ')[0]}

`; // Player's items they can sell const playerItems = {}; for (const item of gameData.inventory) { if (item && item.name && ECONOMY.basePrices[item.name]) { playerItems[item.name] = (playerItems[item.name] || 0) + (item.amount || 1); } } for (const [item, qty] of Object.entries(playerItems)) { const price = getMerchantBuyPrice(selectedMerchant, item); const itemDef = ITEMS[item] || {}; const canMerchantAfford = merchant.gold >= price; html += `
${itemDef.icon || '📦'} ${item} x${qty} ${price}g
`; } html += `
`; tradeDiv.innerHTML = html; } // v6.68: Market tab switching function showMarketTab(tabName) { // Hide all tabs document.querySelectorAll('.market-tab-content').forEach(tab => { tab.style.display = 'none'; }); // Deactivate all tab buttons document.querySelectorAll('#market-modal .codex-tab').forEach(btn => { btn.classList.remove('active'); }); // Show selected tab const selectedTab = document.getElementById(`market-tab-${tabName}`); if (selectedTab) selectedTab.style.display = 'block'; // Activate button const selectedBtn = document.querySelector(`#market-modal .codex-tab[data-tab="${tabName}"]`); if (selectedBtn) selectedBtn.classList.add('active'); // Populate manipulation dropdowns if (tabName === 'manipulate') { populateManipulationDropdowns(); } } // v6.68: Populate manipulation dropdowns with player items function populateManipulationDropdowns() { const floodSelect = document.getElementById('flood-item'); const cornerSelect = document.getElementById('corner-item'); if (floodSelect) { floodSelect.innerHTML = ''; // Add items player has const playerItems = {}; for (const item of gameData.inventory) { if (item && item.name && ECONOMY.basePrices[item.name]) { playerItems[item.name] = (playerItems[item.name] || 0) + (item.amount || 1); } } for (const [item, qty] of Object.entries(playerItems)) { const itemDef = ITEMS[item] || {}; floodSelect.innerHTML += ``; } } if (cornerSelect) { cornerSelect.innerHTML = ''; // Add all tradeable items for (const item of Object.keys(ECONOMY.basePrices)) { const itemDef = ITEMS[item] || {}; const price = getMarketPrice(item); cornerSelect.innerHTML += ``; } } } // v6.68: Execute flood market from UI function executeFloodMarket() { const item = document.getElementById('flood-item').value; const qty = parseInt(document.getElementById('flood-qty').value) || 20; if (!item) { showNotification('Select an item to flood!', 'error'); return; } floodMarket(item, qty); } // v6.68: Execute corner market from UI function executeCornerMarket() { const item = document.getElementById('corner-item').value; if (!item) { showNotification('Select an item to corner!', 'error'); return; } createScarcity(item); } // v6.68: Add gold from various sources (mob kills, POI rewards, etc.) function addGold(amount, source = 'unknown') { ECONOMY.gold += amount; ECONOMY.totalEarned += amount; saveEconomy(); if (worldState.player) { spawnFloater(worldState.player.position, `+${amount}g`, '#ffd700'); } } // v5.2: Get talent points available function getTalentPoints() { const totalLevels = Object.values(gameData.skills).reduce((sum, s) => sum + s.level, 0); const pointsEarned = Math.floor(totalLevels / 5); // 1 point per 5 total skill levels const pointsSpent = getSpentTalentPoints(); return { earned: pointsEarned, spent: pointsSpent, available: pointsEarned - pointsSpent }; } function getSpentTalentPoints() { if (!gameData.talents) gameData.talents = {}; let spent = 0; for (const treeId of Object.keys(TALENT_TREES)) { const treeTalents = gameData.talents[treeId] || {}; for (const [talentId, rank] of Object.entries(treeTalents)) { spent += rank; } } return spent; } function getTalentRank(treeId, talentId) { if (!gameData.talents) gameData.talents = {}; if (!gameData.talents[treeId]) gameData.talents[treeId] = {}; return gameData.talents[treeId][talentId] || 0; } function canUnlockTalent(treeId, talentId) { const tree = TALENT_TREES[treeId]; const talent = tree.talents[talentId]; const currentRank = getTalentRank(treeId, talentId); // Check max rank if (currentRank >= talent.maxRank) return false; // Check points available if (getTalentPoints().available <= 0) return false; // Check prerequisite if (talent.requires) { const reqRank = getTalentRank(treeId, talent.requires); const reqTalent = tree.talents[talent.requires]; if (reqRank < reqTalent.maxRank) return false; } return true; } function unlockTalent(treeId, talentId) { if (!canUnlockTalent(treeId, talentId)) { showNotification('Cannot unlock this talent!', 'error'); return false; } if (!gameData.talents) gameData.talents = {}; if (!gameData.talents[treeId]) gameData.talents[treeId] = {}; gameData.talents[treeId][talentId] = (gameData.talents[treeId][talentId] || 0) + 1; const tree = TALENT_TREES[treeId]; const talent = tree.talents[talentId]; showNotification(`Unlocked ${talent.name}!`, 'success'); AudioSystem.levelUp(); saveGameData(); updateTalentModal(); return true; } function getTalentBonuses() { const bonuses = { damage: 0, maxHp: 0, critChance: 0, lifesteal: 0, abilityDamage: 0, defense: 0, dodgeChance: 0, hpRegen: 0, shieldDuration: 0, deathSave: false, lootBonus: 0, resourceYield: 0, rareFind: 0, xpBonus: 0, doubleBossLoot: false }; for (const [treeId, tree] of Object.entries(TALENT_TREES)) { for (const [talentId, talent] of Object.entries(tree.talents)) { const rank = getTalentRank(treeId, talentId); if (rank > 0) { for (const [stat, value] of Object.entries(talent.effect)) { if (typeof value === 'boolean') { bonuses[stat] = value; } else { bonuses[stat] = (bonuses[stat] || 0) + (value * rank); } } } } } return bonuses; } // ============================================ // v5.13: SHIP DEFENSE SYSTEM // Visible ship on world map with defensive laser // ============================================ const SHIP_STATE = { mesh: null, landingPad: null, hp: 100, maxHp: 100, position: new THREE.Vector3(0, 0, 0), // v7.92: Pre-allocated vectors for beam calculations to avoid clone() per frame _beamStart: new THREE.Vector3(), _beamEnd: new THREE.Vector3(), _beamDir: new THREE.Vector3(), _turretDir: new THREE.Vector3(), // v7.93: Pooled geometry for heal beam to avoid CylinderGeometry allocation per beam _healBeamGeometry: null, laser: { active: false, target: null, beam: null, cooldown: 0, lastFire: 0, damage: 15, range: 35, fireRate: 800, // ms between shots autoDefend: true }, // v6.68: Healing system for player and friendly creeps healing: { enabled: true, range: 25, // Healing range playerHealRate: 2, // HP per second for player creepHealRate: 5, // HP per second for creeps healInterval: 500, // ms between heal ticks lastHealTime: 0, healBeam: null, totalHealed: 0 // Tracking stat }, propellers: [], thrustLight: null, damaged: false, repairCost: 50, // gold to repair // v5.15: Defense tracking system defenseLog: { // Statistics totalEngagements: 0, // Times laser fired totalKills: 0, // Enemies killed by ship totalDamageDealt: 0, // Total damage output entitiesDeterred: 0, // Enemies that fled after being hit timesAttacked: 0, // Times ship was attacked totalDamageTaken: 0, // Total damage received repairsPerformed: 0, // Times repaired totalRepairCost: 0, // Gold spent on repairs timesDestroyed: 0, // Times ship was destroyed // Recent events log (rolling buffer of last 50) events: [], maxEvents: 50 } }; // v5.15: Log a defense event function logDefenseEvent(eventType, data = {}) { const log = SHIP_STATE.defenseLog; const timestamp = Date.now(); const event = { type: eventType, timestamp: timestamp, time: new Date(timestamp).toLocaleTimeString(), ...data }; // Add to events array (rolling buffer) log.events.push(event); if (log.events.length > log.maxEvents) { log.events.shift(); } // Update statistics based on event type switch (eventType) { case 'laser_fired': log.totalEngagements++; log.totalDamageDealt += data.damage || 0; break; case 'enemy_killed': log.totalKills++; break; case 'enemy_deterred': log.entitiesDeterred++; break; case 'ship_attacked': log.timesAttacked++; log.totalDamageTaken += data.damage || 0; break; case 'ship_repaired': log.repairsPerformed++; log.totalRepairCost += data.cost || 0; break; case 'ship_destroyed': log.timesDestroyed++; break; } // Update defense log UI if open updateDefenseLogUI(); return event; } // Get formatted defense statistics function getDefenseStats() { const log = SHIP_STATE.defenseLog; return { engagements: log.totalEngagements, kills: log.totalKills, damageDealt: log.totalDamageDealt, deterred: log.entitiesDeterred, attacked: log.timesAttacked, damageTaken: log.totalDamageTaken, repairs: log.repairsPerformed, repairCost: log.totalRepairCost, destroyed: log.timesDestroyed, killRatio: log.totalEngagements > 0 ? (log.totalKills / log.totalEngagements * 100).toFixed(1) : 0, recentEvents: log.events.slice(-10).reverse() }; } // v5.15: Toggle defense log panel visibility function toggleDefenseLog() { const panel = document.getElementById('defense-stats'); if (panel) { const isVisible = panel.style.display !== 'none'; panel.style.display = isVisible ? 'none' : 'block'; if (!isVisible) { updateDefenseLogUI(); } } } // v5.15: Update defense log UI with current stats and events function updateDefenseLogUI() { const stats = getDefenseStats(); // Update stat displays const updateElement = (id, value) => { const el = document.getElementById(id); if (el) el.textContent = value; }; updateElement('stat-engagements', stats.engagements); updateElement('stat-kills', stats.kills); updateElement('stat-damage-dealt', stats.damageDealt); updateElement('stat-deterred', stats.deterred); updateElement('stat-attacked', stats.attacked); updateElement('stat-damage-taken', stats.damageTaken); updateElement('stat-kill-rate', stats.killRatio + '%'); updateElement('stat-repairs', stats.repairs); // Update events log const eventsLog = document.getElementById('defense-events-log'); if (!eventsLog) return; const events = SHIP_STATE.defenseLog.events; if (events.length === 0) { eventsLog.innerHTML = '
No events yet
'; return; } // Format events (newest first) const eventHTML = events.slice().reverse().map(event => { let icon = '📝'; let color = '#888'; let text = ''; switch (event.type) { case 'laser_fired': icon = '🔫'; color = '#ff8800'; text = `Fired at ${event.targetName || 'enemy'} (${event.damage} dmg)`; break; case 'enemy_killed': icon = '💀'; color = '#ff4444'; text = `Killed ${event.enemyName || 'enemy'} (+${event.threat || 0} threat neutralized)`; break; case 'enemy_deterred': icon = '🏃'; color = '#88ff88'; text = `${event.enemyName || 'Enemy'} fled after sustaining damage`; break; case 'ship_attacked': icon = '⚠️'; color = '#ff6666'; text = `Attacked by ${event.attackerName || 'enemy'} (${event.damage} dmg)`; break; case 'ship_repaired': icon = '🔧'; color = '#ffff88'; text = `Repaired hull (+${event.hpRestored} HP, -${event.cost}g)`; break; case 'ship_destroyed': icon = '💥'; color = '#ff0000'; text = `SHIP DESTROYED by ${event.finalBlow || 'enemy'}!`; break; } return `
${event.time} ${icon} ${text}
`; }).join(''); eventsLog.innerHTML = eventHTML; } // Create ship mesh for world map function createWorldShip(spawnPosition) { const shipGroup = new THREE.Group(); // Main body - sleek fuselage const bodyGeometry = new THREE.BoxGeometry(4, 1.5, 5); const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0x2a2a3a, metalness: 0.7, roughness: 0.3 }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.castShadow = true; body.receiveShadow = true; shipGroup.add(body); // Cockpit dome const cockpitGeometry = new THREE.SphereGeometry(1.2, 16, 16); const cockpitMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, metalness: 0.9, roughness: 0.1, emissive: 0x004444, emissiveIntensity: 0.5 }); const cockpit = new THREE.Mesh(cockpitGeometry, cockpitMaterial); cockpit.position.set(0, 0.8, 0.5); cockpit.scale.set(1, 0.5, 1.2); shipGroup.add(cockpit); // Wings const wingGeometry = new THREE.BoxGeometry(8, 0.2, 2); const wingMaterial = new THREE.MeshStandardMaterial({ color: 0x3a3a4a, metalness: 0.6, roughness: 0.4 }); const wings = new THREE.Mesh(wingGeometry, wingMaterial); wings.position.set(0, 0.3, -0.5); wings.castShadow = true; shipGroup.add(wings); // Wing tips with lights const tipGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.5); const tipMaterialL = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 1 }); const tipMaterialR = new THREE.MeshStandardMaterial({ color: 0x00ff00, emissive: 0x00ff00, emissiveIntensity: 1 }); const tipL = new THREE.Mesh(tipGeometry, tipMaterialL); const tipR = new THREE.Mesh(tipGeometry, tipMaterialR); tipL.position.set(-4, 0.3, -0.5); tipR.position.set(4, 0.3, -0.5); shipGroup.add(tipL, tipR); // Tail fin const tailGeometry = new THREE.BoxGeometry(0.3, 2, 1); const tail = new THREE.Mesh(tailGeometry, wingMaterial); tail.position.set(0, 1, -2); tail.castShadow = true; shipGroup.add(tail); // Engine pods const engineGeometry = new THREE.CylinderGeometry(0.4, 0.5, 2, 8); const engineMaterial = new THREE.MeshStandardMaterial({ color: 0x1a1a2a, metalness: 0.8, roughness: 0.2 }); [-2, 2].forEach(x => { const engine = new THREE.Mesh(engineGeometry, engineMaterial); engine.rotation.x = Math.PI / 2; engine.position.set(x, 0, -2); engine.castShadow = true; shipGroup.add(engine); // Engine glow const glowGeometry = new THREE.CircleGeometry(0.4, 16); const glowMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.8 }); const glow = new THREE.Mesh(glowGeometry, glowMaterial); glow.rotation.x = -Math.PI / 2; glow.position.set(x, 0, -3); shipGroup.add(glow); }); // Laser turret on top const turretBaseGeo = new THREE.CylinderGeometry(0.5, 0.6, 0.4, 8); const turretMaterial = new THREE.MeshStandardMaterial({ color: 0x444455, metalness: 0.8 }); const turretBase = new THREE.Mesh(turretBaseGeo, turretMaterial); turretBase.position.set(0, 1.1, -0.5); shipGroup.add(turretBase); const turretBarrelGeo = new THREE.CylinderGeometry(0.15, 0.15, 1.5, 8); const turretBarrel = new THREE.Mesh(turretBarrelGeo, turretMaterial); turretBarrel.rotation.z = Math.PI / 2; turretBarrel.position.set(0, 1.5, -0.5); shipGroup.add(turretBarrel); shipGroup.userData.turretBarrel = turretBarrel; // Laser beam (initially invisible) const laserGeometry = new THREE.CylinderGeometry(0.05, 0.05, 1, 8); const laserMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.9 }); const laserBeam = new THREE.Mesh(laserGeometry, laserMaterial); laserBeam.visible = false; shipGroup.add(laserBeam); SHIP_STATE.laser.beam = laserBeam; // Landing gear const gearGeometry = new THREE.BoxGeometry(0.3, 0.8, 0.3); const gearMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); [[-1.5, -1, 1], [1.5, -1, 1], [0, -1, -2]].forEach(pos => { const gear = new THREE.Mesh(gearGeometry, gearMaterial); gear.position.set(...pos); gear.castShadow = true; shipGroup.add(gear); }); // Position ship at spawn point (landing zone) shipGroup.position.copy(spawnPosition); shipGroup.position.y = spawnPosition.y + 2; // Slightly above ground shipGroup.rotation.y = Math.random() * Math.PI * 2; SHIP_STATE.mesh = shipGroup; SHIP_STATE.position.copy(spawnPosition); return shipGroup; } // Create landing pad/zone marker function createLandingZone(position) { const padGroup = new THREE.Group(); // Landing pad - circular platform const padGeometry = new THREE.CylinderGeometry(10, 10, 0.3, 32); const padMaterial = new THREE.MeshStandardMaterial({ color: 0x333344, metalness: 0.5, roughness: 0.5 }); const pad = new THREE.Mesh(padGeometry, padMaterial); pad.receiveShadow = true; padGroup.add(pad); // Landing markings - concentric rings [8, 6, 4].forEach((r, i) => { const ringGeo = new THREE.RingGeometry(r - 0.2, r, 32); const ringMat = new THREE.MeshBasicMaterial({ color: i === 0 ? 0xffff00 : 0x00ff00, side: THREE.DoubleSide, transparent: true, opacity: 0.7 }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.rotation.x = -Math.PI / 2; ring.position.y = 0.2; padGroup.add(ring); }); // Corner beacons for (let i = 0; i < 4; i++) { const angle = (i / 4) * Math.PI * 2; const beaconGeo = new THREE.CylinderGeometry(0.3, 0.4, 2, 8); const beaconMat = new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.6 }); const beacon = new THREE.Mesh(beaconGeo, beaconMat); beacon.position.set(Math.cos(angle) * 9, 1, Math.sin(angle) * 9); padGroup.add(beacon); // Beacon light const lightGeo = new THREE.SphereGeometry(0.35, 8, 8); const lightMat = new THREE.MeshBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.9 }); const light = new THREE.Mesh(lightGeo, lightMat); light.position.set(Math.cos(angle) * 9, 2.2, Math.sin(angle) * 9); light.userData.isBeacon = true; light.userData.phase = i * Math.PI / 2; padGroup.add(light); } // HP shield dome (visible when damaged) const shieldGeo = new THREE.SphereGeometry(12, 32, 32); const shieldMat = new THREE.MeshBasicMaterial({ color: 0x00aaff, transparent: true, opacity: 0, side: THREE.DoubleSide, wireframe: true }); const shield = new THREE.Mesh(shieldGeo, shieldMat); shield.position.y = 5; padGroup.add(shield); padGroup.userData.shield = shield; padGroup.position.copy(position); SHIP_STATE.landingPad = padGroup; return padGroup; } // Update ship defense system function updateShipDefense(dt, time) { if (!SHIP_STATE.mesh || mode !== 'world') return; // Animate beacon lights if (SHIP_STATE.landingPad) { SHIP_STATE.landingPad.children.forEach(child => { if (child.userData.isBeacon) { const pulse = (Math.sin(time * 0.003 + child.userData.phase) + 1) / 2; child.material.opacity = 0.5 + pulse * 0.5; } }); // Shield visibility based on recent damage const shield = SHIP_STATE.landingPad.userData.shield; if (shield) { if (SHIP_STATE.damaged) { shield.material.opacity = Math.min(0.3, shield.material.opacity + dt * 0.5); shield.rotation.y += dt * 0.5; } else { shield.material.opacity = Math.max(0, shield.material.opacity - dt * 0.2); } } } // Auto-defend: Find and shoot nearby mobs // v7.72: Use distanceToSquared() to avoid sqrt in hot loop if (SHIP_STATE.laser.autoDefend && time - SHIP_STATE.laser.lastFire > SHIP_STATE.laser.fireRate) { let nearestMob = null; let nearestDistSq = SHIP_STATE.laser.range * SHIP_STATE.laser.range; // v8.04: forEach to for loop conversion (ship defense hot path) const shipMobs = worldState.mobs; for (let i = 0, len = shipMobs.length; i < len; i++) { const mob = shipMobs[i]; if (!mob.parent || mob.userData.hp <= 0) continue; const distSq = SHIP_STATE.mesh.position.distanceToSquared(mob.position); if (distSq < nearestDistSq) { nearestDistSq = distSq; nearestMob = mob; } } if (nearestMob) { fireShipLaser(nearestMob, time); } } // Update laser beam visual updateLaserBeam(dt, time); // v6.68: Ship healing system - heal player and friendly creeps // v7.72: Use distanceToSquared() to avoid sqrt in hot loop if (SHIP_STATE.healing.enabled && time - SHIP_STATE.healing.lastHealTime > SHIP_STATE.healing.healInterval) { SHIP_STATE.healing.lastHealTime = time; const healRange = SHIP_STATE.healing.range; const healRangeSq = healRange * healRange; let healedSomething = false; // Heal player if nearby and damaged // v8.26: Added guard for gameData.player if (worldState.player && gameData?.player?.hp !== undefined && gameData?.player?.maxHp && gameData.player.hp < gameData.player.maxHp) { const playerDistSq = SHIP_STATE.mesh.position.distanceToSquared(worldState.player.position); if (playerDistSq < healRangeSq) { const healAmount = Math.ceil(SHIP_STATE.healing.playerHealRate * (SHIP_STATE.healing.healInterval / 1000)); const actualHeal = Math.min(healAmount, gameData.player.maxHp - gameData.player.hp); gameData.player.hp += actualHeal; SHIP_STATE.healing.totalHealed += actualHeal; updateHealthUI(); // Visual feedback - green healing particles if (actualHeal > 0) { spawnFloater(worldState.player.position, `+${actualHeal}`, '#44ff88'); if (particles) particles.emit(worldState.player.position, 5, 0x44ff88, { spread: 1.5, lifetime: 400, size: 0.15 }); healedSomething = true; // Draw heal beam to player spawnHealBeam(SHIP_STATE.mesh.position, worldState.player.position); } } } // v8.02: forEach to for loop conversion for hot path // Heal friendly creeps (team A) if nearby and damaged if (creepWaveState.creeps) { const shipCreeps = creepWaveState.creeps; const shipCreepsLen = shipCreeps.length; for (let i = 0; i < shipCreepsLen; i++) { const creep = shipCreeps[i]; if (!creep || !creep.userData || creep.userData.team !== 'A') continue; if (creep.userData.hp >= creep.userData.maxHp) continue; const creepDistSq = SHIP_STATE.mesh.position.distanceToSquared(creep.position); if (creepDistSq < healRangeSq) { const healAmount = Math.ceil(SHIP_STATE.healing.creepHealRate * (SHIP_STATE.healing.healInterval / 1000)); const actualHeal = Math.min(healAmount, creep.userData.maxHp - creep.userData.hp); creep.userData.hp += actualHeal; SHIP_STATE.healing.totalHealed += actualHeal; // Update creep HP bar if (creep.userData.hpBar) { const hpPercent = creep.userData.hp / creep.userData.maxHp; creep.userData.hpBar.scale.x = Math.max(0.01, hpPercent); creep.userData.hpBar.material.color.setHex(hpPercent > 0.5 ? 0x00ff00 : hpPercent > 0.25 ? 0xffff00 : 0xff0000); } // Visual feedback for creep healing (less frequent to avoid spam) if (actualHeal > 0 && Math.random() < 0.3) { spawnFloater(creep.position, `+${actualHeal}`, '#44ff88'); if (particles) particles.emit(creep.position, 3, 0x44ff88, { spread: 1, lifetime: 300, size: 0.1 }); spawnHealBeam(SHIP_STATE.mesh.position, creep.position); } healedSomething = true; } } } // Healing aura pulse effect when actively healing if (healedSomething && SHIP_STATE.landingPad) { const shield = SHIP_STATE.landingPad.userData.shield; if (shield) { shield.material.color.setHex(0x44ff88); // Green healing color shield.material.opacity = 0.4; setTimeout(() => { if (shield.material) { shield.material.color.setHex(0x00ffff); // Back to cyan } }, 200); } } } // v8.02: forEach to for loop conversion for hot path // Mobs attacking ship // v7.77: Use distanceToSquared for performance in hot loop const SHIP_ATTACK_RANGE_SQ = 400; // 20 * 20 const PLAYER_FAR_DIST_SQ = 900; // 30 * 30 const SHIP_MELEE_RANGE_SQ = 25; // 5 * 5 const shipMobs = worldState.mobs; const shipMobsLen = shipMobs.length; for (let i = 0; i < shipMobsLen; i++) { const mob = shipMobs[i]; if (!mob.parent || mob.userData.hp <= 0) continue; const distToShipSq = mob.position.distanceToSquared(SHIP_STATE.mesh.position); // Mobs occasionally target ship if player is far away if (distToShipSq < SHIP_ATTACK_RANGE_SQ && !mob.userData.targetingPlayer) { const distToPlayerSq = worldState.player ? mob.position.distanceToSquared(worldState.player.position) : Infinity; if (distToPlayerSq > PLAYER_FAR_DIST_SQ && Math.random() < 0.01) { // Small chance to attack ship mob.userData.targetingShip = true; mob.userData.targetPos.copy(SHIP_STATE.mesh.position); } } // Damage ship when in melee range if (mob.userData.targetingShip && distToShipSq < SHIP_MELEE_RANGE_SQ) { const now = performance.now(); if (!mob.userData.lastShipAttack || now - mob.userData.lastShipAttack > 2000) { // v5.15: Pass mob as attacker for tracking damageShip(mob.userData.damage || 5, mob); mob.userData.lastShipAttack = now; } } } // Gentle hover animation for ship if (SHIP_STATE.mesh) { SHIP_STATE.mesh.position.y = SHIP_STATE.position.y + 2 + Math.sin(time * 0.002) * 0.2; SHIP_STATE.mesh.rotation.z = Math.sin(time * 0.001) * 0.02; } } // Fire ship's defensive laser function fireShipLaser(target, time) { if (!SHIP_STATE.mesh || !target) return; const enemyName = target.userData.name || 'Unknown Entity'; const enemyHpBefore = target.userData.hp; const damage = SHIP_STATE.laser.damage; const distance = SHIP_STATE.mesh.position.distanceTo(target.position); SHIP_STATE.laser.lastFire = time; SHIP_STATE.laser.target = target; SHIP_STATE.laser.active = true; // Rotate turret toward target // v7.92: Use pooled vector instead of new allocation const turret = SHIP_STATE.mesh.userData.turretBarrel; if (turret) { SHIP_STATE._turretDir.subVectors(target.position, SHIP_STATE.mesh.position).normalize(); turret.lookAt(target.position); } // Deal damage target.userData.hp -= damage; // v5.15: Log the laser engagement logDefenseEvent('laser_fired', { enemy: enemyName, damage: damage, distance: Math.round(distance), enemyHpBefore: enemyHpBefore, enemyHpAfter: target.userData.hp, wasTargetingShip: target.userData.targetingShip || false }); // Visual feedback if (particles) { particles.emit(target.position, 10, 0xff0000, { spread: 2, lifetime: 300 }); } spawnFloater(target.position, `-${damage}`, '#ff4444'); // Sound effect placeholder AudioSystem.play('spell'); // Check if killed if (target.userData.hp <= 0) { const xpReward = target.userData.xpReward || 50; addXp('combat', Math.floor(xpReward * 0.5)); // Half XP for ship kills spawnFloater(target.position, `SHIP KILL! +${Math.floor(xpReward * 0.5)}XP`, '#ff8800'); // v5.15: Log the kill logDefenseEvent('enemy_killed', { enemy: enemyName, totalDamageDealt: enemyHpBefore, xpAwarded: Math.floor(xpReward * 0.5), wasTargetingShip: target.userData.targetingShip || false, outcome: 'destroyed' }); } else if (target.userData.targetingShip && target.userData.hp < enemyHpBefore * 0.5) { // v5.15: Check if enemy might flee (deterred) - below 50% HP after being hit // Enemies have a chance to be deterred when significantly damaged if (Math.random() < 0.3) { target.userData.targetingShip = false; target.userData.deterredByShip = true; logDefenseEvent('enemy_deterred', { enemy: enemyName, hpRemaining: target.userData.hp, reason: 'significant_damage', outcome: 'fled' }); spawnFloater(target.position, 'DETERRED!', '#ffaa00'); } } } // Update laser beam visual effect // v7.92: Use pooled vectors instead of clone() per frame function updateLaserBeam(dt, time) { const beam = SHIP_STATE.laser.beam; if (!beam) return; if (SHIP_STATE.laser.active && SHIP_STATE.laser.target) { beam.visible = true; // v7.92: Use pre-allocated vectors instead of clone() SHIP_STATE._beamStart.copy(SHIP_STATE.mesh.position); SHIP_STATE._beamStart.y += 1.5; SHIP_STATE._beamEnd.copy(SHIP_STATE.laser.target.position); SHIP_STATE._beamEnd.y += 1; SHIP_STATE._beamDir.subVectors(SHIP_STATE._beamEnd, SHIP_STATE._beamStart); const length = SHIP_STATE._beamDir.length(); beam.scale.set(1, length, 1); beam.position.copy(SHIP_STATE._beamStart).add(SHIP_STATE._beamDir.multiplyScalar(0.5)); beam.lookAt(SHIP_STATE._beamEnd); beam.rotateX(Math.PI / 2); // Flash effect beam.material.opacity = 0.9; // Deactivate after short duration setTimeout(() => { SHIP_STATE.laser.active = false; beam.visible = false; }, 100); } else { beam.visible = false; } } // v6.68: Spawn a temporary heal beam visual effect // v7.92: Optimized to use pooled vectors and avoid allocations in dir.clone() function spawnHealBeam(startPos, endPos) { if (!scene) return; // v7.92: Use pooled vectors instead of clone() SHIP_STATE._beamStart.copy(startPos); SHIP_STATE._beamStart.y += 3; // From ship turret SHIP_STATE._beamEnd.copy(endPos); SHIP_STATE._beamEnd.y += 1; SHIP_STATE._beamDir.subVectors(SHIP_STATE._beamEnd, SHIP_STATE._beamStart); const length = SHIP_STATE._beamDir.length(); // v7.93: Use pooled geometry for heal beam to avoid CylinderGeometry allocation per beam if (!SHIP_STATE._healBeamGeometry) { SHIP_STATE._healBeamGeometry = new THREE.CylinderGeometry(0.08, 0.08, 1, 6); } const beamMat = new THREE.MeshBasicMaterial({ color: 0x44ff88, transparent: true, opacity: 0.7 }); const healBeam = new THREE.Mesh(SHIP_STATE._healBeamGeometry, beamMat); // Position and orient beam // v7.92: Calculate midpoint offset in-place to avoid clone() const halfLength = length * 0.5; SHIP_STATE._beamDir.normalize().multiplyScalar(halfLength); healBeam.scale.set(1, length, 1); healBeam.position.copy(SHIP_STATE._beamStart).add(SHIP_STATE._beamDir); healBeam.lookAt(SHIP_STATE._beamEnd); healBeam.rotateX(Math.PI / 2); scene.add(healBeam); // Fade out and remove (v7.93: Only dispose material, geometry is pooled) let opacity = 0.7; const fadeInterval = setInterval(() => { opacity -= 0.15; if (opacity <= 0) { clearInterval(fadeInterval); scene.remove(healBeam); beamMat.dispose(); } else { healBeam.material.opacity = opacity; } }, 30); } // Damage the ship // v5.15: Added attacker parameter for tracking function damageShip(amount, attacker = null) { const hpBefore = SHIP_STATE.hp; SHIP_STATE.hp = Math.max(0, SHIP_STATE.hp - amount); SHIP_STATE.damaged = true; // v5.15: Log the attack const attackerName = attacker?.userData?.name || 'Unknown Attacker'; logDefenseEvent('ship_attacked', { attacker: attackerName, damage: amount, shipHpBefore: hpBefore, shipHpAfter: SHIP_STATE.hp, attackerHp: attacker?.userData?.hp || null, critical: amount >= 10 }); // Clear damaged flag after 2 seconds setTimeout(() => { SHIP_STATE.damaged = false; }, 2000); // Visual feedback if (SHIP_STATE.mesh) { // Flash red SHIP_STATE.mesh.children.forEach(child => { if (child.material && child.material.emissive) { const originalColor = child.material.emissive.getHex(); child.material.emissive.setHex(0xff0000); setTimeout(() => { child.material.emissive.setHex(originalColor); }, 200); } }); } // Particles if (particles && SHIP_STATE.mesh) { particles.emit(SHIP_STATE.mesh.position, 15, 0xff4400, { spread: 3, lifetime: 500 }); } spawnFloater(SHIP_STATE.mesh.position, `-${amount}`, '#ff0000'); showNotification(`Ship taking damage! (${SHIP_STATE.hp}/${SHIP_STATE.maxHp} HP)`, 'warning'); // Update UI updateShipHPUI(); // Ship destroyed if (SHIP_STATE.hp <= 0) { shipDestroyed(attackerName); } } // Handle ship destruction // v5.15: Added finalBlow parameter function shipDestroyed(finalBlow = 'Unknown') { // v5.15: Log destruction event logDefenseEvent('ship_destroyed', { finalBlow: finalBlow, totalDamageTaken: SHIP_STATE.defenseLog.totalDamageTaken, totalEngagements: SHIP_STATE.defenseLog.totalEngagements, totalKills: SHIP_STATE.defenseLog.totalKills }); showNotification('SHIP DESTROYED! Repair required to leave planet.', 'error'); // Disable ship mesh if (SHIP_STATE.mesh) { SHIP_STATE.mesh.children.forEach(child => { if (child.material) { child.material.opacity = 0.3; child.material.transparent = true; } }); } // Large explosion if (particles && SHIP_STATE.mesh) { particles.emit(SHIP_STATE.mesh.position, 50, 0xff4400, { spread: 8, lifetime: 1500 }); particles.emit(SHIP_STATE.mesh.position, 30, 0xffff00, { spread: 6, lifetime: 1000 }); } } // Repair ship (costs gold) function repairShip() { if (SHIP_STATE.hp >= SHIP_STATE.maxHp) { showNotification('Ship is already at full health!', 'info'); return; } if (gameData.currency >= SHIP_STATE.repairCost) { const hpBefore = SHIP_STATE.hp; const hpRestored = SHIP_STATE.maxHp - hpBefore; gameData.currency -= SHIP_STATE.repairCost; SHIP_STATE.hp = SHIP_STATE.maxHp; // v5.15: Log repair event logDefenseEvent('ship_repaired', { cost: SHIP_STATE.repairCost, hpBefore: hpBefore, hpAfter: SHIP_STATE.maxHp, hpRestored: hpRestored, wasDestroyed: hpBefore === 0 }); // Restore ship visuals if (SHIP_STATE.mesh) { SHIP_STATE.mesh.children.forEach(child => { if (child.material) { child.material.opacity = 1; child.material.transparent = false; } }); } showNotification(`Ship repaired! -${SHIP_STATE.repairCost} Gold`, 'success'); updateShipHPUI(); saveGameData(); // v6.41: Fixed undefined function call (was saveGame) } else { showNotification(`Need ${SHIP_STATE.repairCost} Gold to repair ship!`, 'warning'); } } // Update ship HP UI // v7.71: Use cached DOM references to avoid getElementById calls function updateShipHPUI() { const cache = getUICache(); const bar = cache.shipHpFill; const text = cache.shipHpText; if (bar) { const percent = (SHIP_STATE.hp / SHIP_STATE.maxHp) * 100; bar.style.width = `${percent}%`; bar.style.background = percent > 50 ? '#00ff88' : percent > 25 ? '#ffaa00' : '#ff4444'; } if (text) { text.textContent = `${SHIP_STATE.hp}/${SHIP_STATE.maxHp}`; } } // Toggle ship auto-defend function toggleShipAutoDefend() { SHIP_STATE.laser.autoDefend = !SHIP_STATE.laser.autoDefend; showNotification(`Ship Auto-Defense: ${SHIP_STATE.laser.autoDefend ? 'ENABLED' : 'DISABLED'}`, 'info'); const btn = document.getElementById('ship-defense-btn'); if (btn) { btn.textContent = SHIP_STATE.laser.autoDefend ? '🛡️ Defense: ON' : '🛡️ Defense: OFF'; btn.style.background = SHIP_STATE.laser.autoDefend ? 'rgba(0, 255, 136, 0.2)' : 'rgba(255, 68, 68, 0.2)'; } } // v5.11: RTS Panel Toggle System const rtsPanelState = { skills: false, crafting: false, inventory: false, equipment: false }; function toggleRTSPanel(panelName) { rtsPanelState[panelName] = !rtsPanelState[panelName]; const panelIds = { skills: 'skills-panel', crafting: 'crafting-panel', inventory: 'inventory-panel', equipment: 'equipment-panel' }; const panel = document.getElementById(panelIds[panelName]); const toggleBtn = document.getElementById(`toggle-${panelName}`); if (panel) { if (rtsPanelState[panelName]) { panel.classList.add('visible'); // v7.39: Focus management - move focus to panel when opened (Cycle 18 UX/Accessibility) // Find first focusable element in panel or use close button requestAnimationFrame(() => { const closeBtn = panel.querySelector('.close-panel'); const firstFocusable = panel.querySelector('button, [tabindex="0"], input, select, textarea'); if (firstFocusable) { firstFocusable.focus(); } else if (closeBtn) { closeBtn.focus(); } }); } else { panel.classList.remove('visible'); // v7.39: Return focus to toggle button when panel closes (Cycle 18 UX/Accessibility) if (toggleBtn) { toggleBtn.focus(); } } } if (toggleBtn) { if (rtsPanelState[panelName]) { toggleBtn.classList.add('active'); toggleBtn.setAttribute('aria-expanded', 'true'); // v7.37: WCAG 4.1.2 state sync } else { toggleBtn.classList.remove('active'); toggleBtn.setAttribute('aria-expanded', 'false'); // v7.37: WCAG 4.1.2 state sync } } } // v8.32: Swipe Gestures for Mobile Panel Navigation // Allows users to swipe left/right to cycle through panels on touch devices const SwipeGestures = { startX: 0, startY: 0, startTime: 0, minSwipeDistance: 80, // Minimum pixels to register as swipe maxSwipeTime: 500, // Maximum ms for a valid swipe panels: ['skills', 'crafting', 'inventory', 'equipment'], currentPanelIndex: -1, // -1 means no panel open init() { // Only initialize on touch devices if (!('ontouchstart' in window)) return; document.addEventListener('touchstart', (e) => this.handleTouchStart(e), { passive: true }); document.addEventListener('touchend', (e) => this.handleTouchEnd(e), { passive: true }); debugLog('SwipeGestures', 'Mobile swipe navigation initialized'); }, handleTouchStart(e) { // Don't capture swipes on interactive elements const target = e.target; if (target.closest('button, input, select, textarea, .joystick-knob, canvas')) return; this.startX = e.touches[0].clientX; this.startY = e.touches[0].clientY; this.startTime = Date.now(); }, handleTouchEnd(e) { if (!this.startTime) return; const endX = e.changedTouches[0].clientX; const endY = e.changedTouches[0].clientY; const deltaX = endX - this.startX; const deltaY = endY - this.startY; const elapsed = Date.now() - this.startTime; // Reset for next gesture this.startTime = 0; // Check if it's a valid horizontal swipe (not vertical scroll) if (Math.abs(deltaX) < this.minSwipeDistance) return; if (Math.abs(deltaY) > Math.abs(deltaX) * 0.7) return; // Too vertical if (elapsed > this.maxSwipeTime) return; // Determine current panel state this.updateCurrentPanelIndex(); if (deltaX > 0) { // Swipe right - previous panel or close this.navigateToPreviousPanel(); } else { // Swipe left - next panel this.navigateToNextPanel(); } // Haptic feedback if (typeof MobileHaptics !== 'undefined') { MobileHaptics.vibrate('light'); } }, updateCurrentPanelIndex() { // Find which panel is currently open for (let i = 0; i < this.panels.length; i++) { if (rtsPanelState[this.panels[i]]) { this.currentPanelIndex = i; return; } } this.currentPanelIndex = -1; }, navigateToNextPanel() { // Close current panel if open if (this.currentPanelIndex >= 0) { toggleRTSPanel(this.panels[this.currentPanelIndex]); } // Open next panel (or first if none open) const nextIndex = (this.currentPanelIndex + 1) % this.panels.length; toggleRTSPanel(this.panels[nextIndex]); this.currentPanelIndex = nextIndex; showNotification(`Swipe: ${this.panels[nextIndex].charAt(0).toUpperCase() + this.panels[nextIndex].slice(1)} Panel`, 'info'); }, navigateToPreviousPanel() { if (this.currentPanelIndex < 0) { // No panel open, open last one const lastIndex = this.panels.length - 1; toggleRTSPanel(this.panels[lastIndex]); this.currentPanelIndex = lastIndex; } else if (this.currentPanelIndex === 0) { // First panel, close it toggleRTSPanel(this.panels[0]); this.currentPanelIndex = -1; showNotification('Panels closed', 'info'); return; } else { // Navigate to previous panel toggleRTSPanel(this.panels[this.currentPanelIndex]); const prevIndex = this.currentPanelIndex - 1; toggleRTSPanel(this.panels[prevIndex]); this.currentPanelIndex = prevIndex; } if (this.currentPanelIndex >= 0) { showNotification(`Swipe: ${this.panels[this.currentPanelIndex].charAt(0).toUpperCase() + this.panels[this.currentPanelIndex].slice(1)} Panel`, 'info'); } } }; // Initialize swipe gestures when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => SwipeGestures.init()); } else { SwipeGestures.init(); } window.SwipeGestures = SwipeGestures; // v5.11: Keyboard shortcuts for RTS panels // v6.35: Fixed 'E' conflict - equipment now uses 'G' for Gear, 'E' reserved for combat ability // v7.0: Enhanced hotkey handler with consistent shortcuts function handleRTSPanelHotkeys(e) { if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return; switch(e.key.toLowerCase()) { case 'k': toggleRTSPanel('skills'); break; case 'i': toggleRTSPanel('inventory'); break; case 'g': toggleRTSPanel('equipment'); break; case 'p': toggleRTSPanel('crafting'); break; // P for Production/Crafting case 'm': // M for Map/Galaxy if (currentMode === 'world' && typeof openGalaxyManager === 'function') { openGalaxyManager(); } break; case 'n': // v7.4: N for Navigate/Quick Travel (8-Strategy Consensus Cycle 2) if (typeof QuickTravelSystem !== 'undefined') { QuickTravelSystem.toggle(); MobileHaptics.vibrate('menuOpen'); } break; case 'u': // v10.30: U for Unified HUD toggle if (typeof UnifiedHUD !== 'undefined' && currentMode === 'world') { UnifiedHUD.toggle(); showNotification(UnifiedHUD.active ? '🎮 Unified HUD enabled' : '🎮 Classic HUD restored', 'info'); } break; } } // v5.2: Talent Modal UI // v8.24: Added null safety check for modal element function showTalentModal() { const modal = document.getElementById('talent-modal'); if (modal) modal.style.display = 'flex'; updateTalentModal(); } function closeTalentModal() { const modal = document.getElementById('talent-modal'); if (modal) modal.style.display = 'none'; } function updateTalentModal() { const points = getTalentPoints(); document.getElementById('talent-points-display').textContent = `Talent Points: ${points.available}/${points.earned}`; for (const [treeId, tree] of Object.entries(TALENT_TREES)) { const treeDiv = document.getElementById(`talent-tree-${treeId}`); if (!treeDiv) continue; let html = ''; for (const [talentId, talent] of Object.entries(tree.talents)) { const rank = getTalentRank(treeId, talentId); const canUnlock = canUnlockTalent(treeId, talentId); const isMaxed = rank >= talent.maxRank; const isLocked = talent.requires && getTalentRank(treeId, talent.requires) < TALENT_TREES[treeId].talents[talent.requires].maxRank; html += `
${talent.name} ${rank}/${talent.maxRank}
${talent.desc}
`; } treeDiv.innerHTML = html; } } // ============================================ // v5.3: MASTERY SYSTEM // ============================================ const MASTERY_MILESTONES = { mining: { name: 'Mining', icon: '⛏️', color: '#888888', milestones: [ { level: 5, reward: { type: 'bonus', stat: 'miningYield', value: 0.1 }, desc: '+10% ore yield' }, { level: 10, reward: { type: 'bonus', stat: 'miningYield', value: 0.15 }, desc: '+15% ore yield' }, { level: 15, reward: { type: 'unlock', item: 'Miner\'s Blessing' }, desc: 'Unlock Miner\'s Blessing buff' }, { level: 20, reward: { type: 'bonus', stat: 'miningYield', value: 0.25 }, desc: '+25% ore yield' }, { level: 25, reward: { type: 'title', title: 'Grandmaster Miner' }, desc: 'Earn Grandmaster title' } ] }, wood: { name: 'Woodcutting', icon: '🪓', color: '#da5500', milestones: [ { level: 5, reward: { type: 'bonus', stat: 'woodYield', value: 0.1 }, desc: '+10% wood yield' }, { level: 10, reward: { type: 'bonus', stat: 'woodYield', value: 0.15 }, desc: '+15% wood yield' }, { level: 15, reward: { type: 'unlock', item: 'Lumberjack\'s Spirit' }, desc: 'Unlock Lumberjack buff' }, { level: 20, reward: { type: 'bonus', stat: 'woodYield', value: 0.25 }, desc: '+25% wood yield' }, { level: 25, reward: { type: 'title', title: 'Grandmaster Lumberjack' }, desc: 'Earn Grandmaster title' } ] }, combat: { name: 'Combat', icon: '⚔️', color: '#ff4444', milestones: [ { level: 5, reward: { type: 'bonus', stat: 'combatDamage', value: 0.05 }, desc: '+5% damage' }, { level: 10, reward: { type: 'bonus', stat: 'combatDamage', value: 0.1 }, desc: '+10% damage' }, { level: 15, reward: { type: 'unlock', ability: 'Veteran Strike' }, desc: 'Unlock Veteran Strike' }, { level: 20, reward: { type: 'bonus', stat: 'combatCrit', value: 0.05 }, desc: '+5% crit chance' }, { level: 25, reward: { type: 'title', title: 'Warlord' }, desc: 'Earn Warlord title' } ] }, fishing: { name: 'Fishing', icon: '🎣', color: '#4488ff', milestones: [ { level: 5, reward: { type: 'bonus', stat: 'fishChance', value: 0.1 }, desc: '+10% catch rate' }, { level: 10, reward: { type: 'bonus', stat: 'rareFind', value: 0.05 }, desc: '+5% rare fish' }, { level: 15, reward: { type: 'unlock', item: 'Golden Lure' }, desc: 'Unlock Golden Lure' }, { level: 20, reward: { type: 'bonus', stat: 'fishChance', value: 0.2 }, desc: '+20% catch rate' }, { level: 25, reward: { type: 'title', title: 'Master Angler' }, desc: 'Earn Master title' } ] }, cooking: { name: 'Cooking', icon: '🍳', color: '#ff8800', milestones: [ { level: 5, reward: { type: 'bonus', stat: 'healBonus', value: 0.1 }, desc: '+10% heal amount' }, { level: 10, reward: { type: 'bonus', stat: 'healBonus', value: 0.15 }, desc: '+15% heal amount' }, { level: 15, reward: { type: 'unlock', recipe: 'Feast' }, desc: 'Unlock Feast recipe' }, { level: 20, reward: { type: 'bonus', stat: 'foodDuration', value: 0.3 }, desc: '+30% buff duration' }, { level: 25, reward: { type: 'title', title: 'Master Chef' }, desc: 'Earn Master title' } ] }, crafting: { name: 'Crafting', icon: '🔨', color: '#aa44ff', milestones: [ { level: 5, reward: { type: 'bonus', stat: 'craftBonus', value: 0.1 }, desc: '+10% craft success' }, { level: 10, reward: { type: 'bonus', stat: 'materialSave', value: 0.1 }, desc: '10% material savings' }, { level: 15, reward: { type: 'unlock', recipe: 'Masterwork Forge' }, desc: 'Unlock Masterwork crafts' }, { level: 20, reward: { type: 'bonus', stat: 'rarityBoost', value: 0.15 }, desc: '+15% rarity chance' }, { level: 25, reward: { type: 'title', title: 'Artisan Supreme' }, desc: 'Earn Artisan title' } ] } }; function getMasteryBonuses() { const bonuses = { miningYield: 0, woodYield: 0, combatDamage: 0, combatCrit: 0, fishChance: 0, rareFind: 0, healBonus: 0, foodDuration: 0, craftBonus: 0, materialSave: 0, rarityBoost: 0 }; for (const [skillId, mastery] of Object.entries(MASTERY_MILESTONES)) { const skillLevel = gameData.skills[skillId]?.level || 1; for (const milestone of mastery.milestones) { if (skillLevel >= milestone.level && milestone.reward.type === 'bonus') { bonuses[milestone.reward.stat] = (bonuses[milestone.reward.stat] || 0) + milestone.reward.value; } } } return bonuses; } function getUnlockedMasteryTitles() { const titles = []; for (const [skillId, mastery] of Object.entries(MASTERY_MILESTONES)) { const skillLevel = gameData.skills[skillId]?.level || 1; for (const milestone of mastery.milestones) { if (skillLevel >= milestone.level && milestone.reward.type === 'title') { titles.push(milestone.reward.title); } } } return titles; } // v8.24: Added null safety check for modal element function openMasteryModal() { const modal = document.getElementById('mastery-modal'); if (modal) modal.style.display = 'flex'; updateMasteryModal(); } function closeMasteryModal() { const modal = document.getElementById('mastery-modal'); if (modal) modal.style.display = 'none'; } function updateMasteryModal() { const listDiv = document.getElementById('mastery-list'); let html = ''; for (const [skillId, mastery] of Object.entries(MASTERY_MILESTONES)) { const skillLevel = gameData.skills[skillId]?.level || 1; const maxMilestone = mastery.milestones[mastery.milestones.length - 1].level; const progress = Math.min(100, (skillLevel / maxMilestone) * 100); const isMastered = skillLevel >= maxMilestone; html += `
${mastery.icon} ${mastery.name} Lv ${skillLevel}
`; for (const milestone of mastery.milestones) { const achieved = skillLevel >= milestone.level; const isNext = !achieved && mastery.milestones.find(m => skillLevel < m.level)?.level === milestone.level; html += `
Lv${milestone.level}: ${achieved ? '✓' : milestone.desc.substring(0, 15)}...
`; } html += `
`; } listDiv.innerHTML = html; } // ============================================ // v5.3: REALM PORTAL SYSTEM // ============================================ const REALM_PORTALS = { shadow_realm: { name: 'Shadow Realm', icon: '🌑', tier: 1, desc: 'A realm of darkness where shadows come alive. Enhanced enemy spawn rates.', requirements: { combatLevel: 10, bossesDefeated: 1 }, modifiers: { enemyDamage: 1.5, enemyHp: 1.3, spawnRate: 2.0 }, rewards: ['Shadow Essence', 'Dark Crystal'], xpMultiplier: 1.5, duration: 300 // 5 minutes }, frost_dimension: { name: 'Frost Dimension', icon: '❄️', tier: 2, desc: 'An eternally frozen world. All enemies inflict chill. Ice enemies are empowered.', requirements: { combatLevel: 15, bossesDefeated: 3 }, modifiers: { enemyDamage: 1.8, enemyHp: 1.5, allEnemiesChill: true }, rewards: ['Frozen Heart', 'Permafrost Shard', 'Frost Blade'], xpMultiplier: 2.0, duration: 300 }, inferno_pit: { name: 'Inferno Pit', icon: '🔥', tier: 2, desc: 'Volcanic realm of eternal flame. Fire damage over time. Magma enemies empowered.', requirements: { combatLevel: 15, bossesDefeated: 3 }, modifiers: { enemyDamage: 2.0, enemyHp: 1.5, environmentalDamage: 2 }, rewards: ['Infernal Core', 'Magma Heart', 'Magma Sword'], xpMultiplier: 2.0, duration: 300 }, void_nexus: { name: 'Void Nexus', icon: '🌀', tier: 3, desc: 'The space between dimensions. Reality warps around you. Elite enemies guaranteed.', requirements: { combatLevel: 20, bossesDefeated: 5, elitesKilled: 20 }, modifiers: { enemyDamage: 2.5, enemyHp: 2.0, allElites: true }, rewards: ['Void Core', 'Dimension Shard', 'Void Dagger', 'Legendary Core'], xpMultiplier: 3.0, duration: 300 }, celestial_ascent: { name: 'Celestial Ascent', icon: '✨', tier: 4, desc: 'The ultimate challenge. Face the Celestial Guardians in their domain.', requirements: { combatLevel: 25, bossesDefeated: 10, portalClears: 5 }, modifiers: { enemyDamage: 3.0, enemyHp: 3.0, bossOnly: true }, rewards: ['Celestial Essence', 'Star Fragment', 'Legendary Blade', 'Mythic Orb'], xpMultiplier: 5.0, duration: 600 // 10 minutes } }; function initPortalSystem() { if (!gameData.portals) { gameData.portals = { clears: {}, currentPortal: null, portalStartTime: 0, totalClears: 0 }; } } function canEnterPortal(portalId) { const portal = REALM_PORTALS[portalId]; if (!portal) return false; const reqs = portal.requirements; const combatLevel = gameData.skills?.combat?.level || 1; const bossesDefeated = gameData.statistics?.bossesDefeated || 0; const elitesKilled = gameData.statistics?.elitesKilled || 0; const portalClears = gameData.portals?.totalClears || 0; if (combatLevel < reqs.combatLevel) return false; if (bossesDefeated < reqs.bossesDefeated) return false; if (reqs.elitesKilled && elitesKilled < reqs.elitesKilled) return false; if (reqs.portalClears && portalClears < reqs.portalClears) return false; return true; } function getPortalRequirementText(portalId) { const portal = REALM_PORTALS[portalId]; const reqs = portal.requirements; const parts = []; parts.push(`Combat Lv ${reqs.combatLevel}`); parts.push(`${reqs.bossesDefeated} bosses`); if (reqs.elitesKilled) parts.push(`${reqs.elitesKilled} elites`); if (reqs.portalClears) parts.push(`${reqs.portalClears} portal clears`); return parts.join(' | '); } function enterPortal(portalId) { if (!canEnterPortal(portalId)) { showNotification('Requirements not met!', 'error'); return false; } if (gameData.portals.currentPortal) { showNotification('Already in a portal realm!', 'warning'); return false; } if (mode !== 'world') { showNotification('Must be on a planet to enter portals!', 'warning'); return false; } const portal = REALM_PORTALS[portalId]; gameData.portals.currentPortal = portalId; gameData.portals.portalStartTime = Date.now(); gameData.portals.killProgress = 0; // v5.3: Reset kill counter showNotification(`Entered ${portal.name}! ${portal.duration / 60} minutes to clear.`, 'success'); AudioSystem.bossSpawn(); if (particles && worldState.player) { particles.emit(worldState.player.position, 50, parseInt(portal.icon === '🌑' ? '0x440088' : portal.icon === '❄️' ? '0x88ddff' : portal.icon === '🔥' ? '0xff4400' : '0x8844ff'), { spread: 8, lifetime: 1500 }); } closePortalModal(); updatePortalUI(); saveGameData(); return true; } function exitPortal(completed = false) { if (!gameData.portals.currentPortal) return; const portalId = gameData.portals.currentPortal; const portal = REALM_PORTALS[portalId]; if (completed) { // Grant rewards gameData.portals.clears[portalId] = (gameData.portals.clears[portalId] || 0) + 1; gameData.portals.totalClears++; // Give a random reward const rewardItem = portal.rewards[Math.floor(Math.random() * portal.rewards.length)]; addItem(rewardItem); // v6.35: Chronicle Engine - capture portal clear if (typeof captureChronicleEvent === 'function') { captureChronicleEvent('portal_cleared', { portalName: portal.name, reward: rewardItem, totalClears: gameData.portals.totalClears }); } showNotification(`Portal cleared! Received ${rewardItem}!`, 'success'); AudioSystem.levelUp(); if (particles && worldState.player) { particles.emit(worldState.player.position, 60, 0xffd700, { spread: 10, lifetime: 2000 }); } } else { showNotification('Portal expired. Try again!', 'warning'); } gameData.portals.currentPortal = null; gameData.portals.portalStartTime = 0; updatePortalUI(); saveGameData(); } function getPortalModifiers() { if (!gameData.portals?.currentPortal) return null; return REALM_PORTALS[gameData.portals.currentPortal]?.modifiers || null; } function getPortalXpMultiplier() { if (!gameData.portals?.currentPortal) return 1; return REALM_PORTALS[gameData.portals.currentPortal]?.xpMultiplier || 1; } function checkPortalTimeout() { if (!gameData.portals?.currentPortal) return; const portal = REALM_PORTALS[gameData.portals.currentPortal]; const elapsed = (Date.now() - gameData.portals.portalStartTime) / 1000; if (elapsed >= portal.duration) { exitPortal(false); } } function getPortalTimeRemaining() { if (!gameData.portals?.currentPortal) return 0; const portal = REALM_PORTALS[gameData.portals.currentPortal]; const elapsed = (Date.now() - gameData.portals.portalStartTime) / 1000; return Math.max(0, portal.duration - elapsed); } // v8.24: Added null safety check for modal element function openPortalModal() { initPortalSystem(); const modal = document.getElementById('portal-modal'); if (modal) modal.style.display = 'flex'; updatePortalModal(); } function closePortalModal() { const modal = document.getElementById('portal-modal'); if (modal) modal.style.display = 'none'; } function updatePortalModal() { const currentPortal = gameData.portals?.currentPortal; document.getElementById('current-realm').textContent = currentPortal ? REALM_PORTALS[currentPortal].name : 'None'; const listDiv = document.getElementById('portal-list'); let html = ''; for (const [portalId, portal] of Object.entries(REALM_PORTALS)) { const canEnter = canEnterPortal(portalId); const isActive = currentPortal === portalId; const clears = gameData.portals?.clears?.[portalId] || 0; html += `
${portal.icon} ${portal.name} Tier ${portal.tier}
${portal.desc}
${portal.rewards.map(r => `${ITEMS[r]?.icon || '📦'} ${r}`).join('')}
${canEnter ? `✓ Unlocked | Cleared: ${clears}x | ${portal.xpMultiplier}x XP` : `🔒 ${getPortalRequirementText(portalId)}`}
${isActive ? `
⏱️ Active - ${Math.floor(getPortalTimeRemaining())}s remaining
` : ''}
`; } listDiv.innerHTML = html; } function updatePortalUI() { // This would update any in-game portal indicators if (document.getElementById('portal-modal').style.display === 'flex') { updatePortalModal(); } } // ============================================ // v5.3: LOOT RARITY SYSTEM // ============================================ const LOOT_RARITIES = { common: { name: 'Common', color: '#aaaaaa', chance: 0.60, statMult: 1.0 }, uncommon: { name: 'Uncommon', color: '#44ff44', chance: 0.25, statMult: 1.15 }, rare: { name: 'Rare', color: '#4488ff', chance: 0.10, statMult: 1.35 }, epic: { name: 'Epic', color: '#aa44ff', chance: 0.04, statMult: 1.6 }, legendary: { name: 'Legendary', color: '#ff8800', chance: 0.0095, statMult: 2.0 }, mythic: { name: 'Mythic', color: '#ff4488', chance: 0.0005, statMult: 3.0 } }; const ITEM_MODIFIERS = { // Offensive modifiers sharp: { name: 'Sharp', stat: 'damage', value: 3, desc: '+3 Damage' }, keen: { name: 'Keen', stat: 'critChance', value: 0.05, desc: '+5% Crit' }, brutal: { name: 'Brutal', stat: 'damage', value: 5, desc: '+5 Damage' }, deadly: { name: 'Deadly', stat: 'critDamage', value: 0.25, desc: '+25% Crit Damage' }, vampiric: { name: 'Vampiric', stat: 'lifesteal', value: 0.05, desc: '+5% Lifesteal' }, // Defensive modifiers sturdy: { name: 'Sturdy', stat: 'defense', value: 2, desc: '+2 Defense' }, fortified: { name: 'Fortified', stat: 'defense', value: 4, desc: '+4 Defense' }, vital: { name: 'Vital', stat: 'maxHp', value: 15, desc: '+15 Max HP' }, resilient: { name: 'Resilient', stat: 'damageReduction', value: 0.05, desc: '+5% DR' }, // Utility modifiers swift: { name: 'Swift', stat: 'moveSpeed', value: 0.1, desc: '+10% Speed' }, lucky: { name: 'Lucky', stat: 'lootBonus', value: 0.1, desc: '+10% Loot' }, wise: { name: 'Wise', stat: 'xpBonus', value: 0.1, desc: '+10% XP' }, efficient: { name: 'Efficient', stat: 'resourceYield', value: 0.15, desc: '+15% Yield' } }; function rollItemRarity(baseLuckBonus = 0) { const masteryBonuses = getMasteryBonuses(); const talentBonuses = getTalentBonuses(); const totalLuck = baseLuckBonus + (masteryBonuses.rarityBoost || 0) + (talentBonuses.rareFind || 0); let roll = Math.random(); // Luck improves rare+ chances roll = roll * (1 - totalLuck); let cumulative = 0; for (const [rarityId, rarity] of Object.entries(LOOT_RARITIES)) { cumulative += rarity.chance; if (roll < cumulative) { return rarityId; } } return 'common'; } function rollItemModifiers(rarity) { const numModifiers = { common: 0, uncommon: 1, rare: 1, epic: 2, legendary: 2, mythic: 3 }; const count = numModifiers[rarity] || 0; if (count === 0) return []; const modifierKeys = Object.keys(ITEM_MODIFIERS); const selected = []; for (let i = 0; i < count; i++) { const availableModifiers = modifierKeys.filter(m => !selected.includes(m)); if (availableModifiers.length === 0) break; const modId = availableModifiers[Math.floor(Math.random() * availableModifiers.length)]; selected.push(modId); } return selected; } function createRarityItem(baseItemName, forcedRarity = null) { const rarity = forcedRarity || rollItemRarity(); const modifiers = rollItemModifiers(rarity); const rarityData = LOOT_RARITIES[rarity]; return { baseName: baseItemName, rarity: rarity, modifiers: modifiers, statMultiplier: rarityData.statMult }; } function getRarityItemName(rarityItem) { if (!rarityItem || !rarityItem.rarity || rarityItem.rarity === 'common') { return rarityItem?.baseName || rarityItem; } const modNames = rarityItem.modifiers?.map(m => ITEM_MODIFIERS[m]?.name).filter(Boolean) || []; const prefix = modNames.length > 0 ? modNames.join(' ') + ' ' : ''; const rarityData = LOOT_RARITIES[rarityItem.rarity]; return `${prefix}${rarityItem.baseName}`; } function getRarityItemStats(rarityItem) { if (!rarityItem || typeof rarityItem === 'string') return {}; const stats = {}; const mult = rarityItem.statMultiplier || 1; // Apply modifier stats for (const modId of (rarityItem.modifiers || [])) { const mod = ITEM_MODIFIERS[modId]; if (mod) { stats[mod.stat] = (stats[mod.stat] || 0) + mod.value; } } return stats; } function showRarityDropPopup(rarityItem) { if (!rarityItem || rarityItem.rarity === 'common') return; const rarityData = LOOT_RARITIES[rarityItem.rarity]; const itemData = ITEMS[rarityItem.baseName] || {}; const displayName = getRarityItemName(rarityItem); const modifierStats = getRarityItemStats(rarityItem); // v6.35: Chronicle Engine - capture rare item drops (legendary/epic only) if (typeof captureChronicleEvent === 'function' && (rarityItem.rarity === 'legendary' || rarityItem.rarity === 'epic')) { captureChronicleEvent('rare_item', { itemName: displayName, rarity: rarityItem.rarity, baseName: rarityItem.baseName }); } // Create popup const popup = document.createElement('div'); popup.className = 'loot-drop-popup'; popup.innerHTML = `
${itemData.icon || '📦'}
${rarityData.name} Drop!
${displayName}
${Object.keys(modifierStats).length > 0 ? `
${rarityItem.modifiers.map(m => ITEM_MODIFIERS[m]?.desc).join(' | ')}
` : ''} `; document.body.appendChild(popup); // Auto-remove after 5 seconds setTimeout(() => { if (popup.parentElement) { popup.remove(); } }, 5000); // Play appropriate sound if (rarityItem.rarity === 'legendary' || rarityItem.rarity === 'mythic') { AudioSystem.levelUp(); // v8.30: Add VisualFeedback for legendary/mythic drops if (typeof VisualFeedback !== 'undefined') { VisualFeedback.successBurst(rarityData.color || '#ffd700'); VisualFeedback.shake(8, 300); } } else if (rarityItem.rarity === 'epic') { AudioSystem.collect(); // v8.30: Add VisualFeedback for epic drops if (typeof VisualFeedback !== 'undefined') { VisualFeedback.successBurst(rarityData.color || '#a335ee'); VisualFeedback.shake(4, 200); } } else { AudioSystem.collect(); } } // Enhanced item drop function that uses rarity system function dropRarityItem(baseItemName, luckBonus = 0) { const rarityItem = createRarityItem(baseItemName, null); // Store rarity items in a special format if (!gameData.rarityItems) gameData.rarityItems = []; if (rarityItem.rarity !== 'common') { gameData.rarityItems.push(rarityItem); showRarityDropPopup(rarityItem); } // Add the base item to inventory (rarity tracked separately) addItem(baseItemName); return rarityItem; } // Get total bonus stats from all rarity items function getRarityBonuses() { const bonuses = {}; for (const item of (gameData.rarityItems || [])) { // Only count equipped items (check if base item is in equipment) const gear = getEquippedGear(); const isEquipped = Object.values(gear).some(g => g === item.baseName); if (isEquipped) { const stats = getRarityItemStats(item); for (const [stat, value] of Object.entries(stats)) { bonuses[stat] = (bonuses[stat] || 0) + value; } } } return bonuses; } // v5.5: 3D Ship Landing Mini-Game System (Drone-style) let landingGame = { active: false, targetCiv: null, scene: null, camera: null, renderer: null, ship: null, landingPad: null, animFrame: null, lastTime: 0, isManual: false, fuel: 100, velocity: null, targetPosition: null, propellers: [], thrustLight: null, environmentObjects: [], _tempVelocity: new THREE.Vector3(), // v7.84: Pre-allocated for velocity updates _autopilotPadPos: new THREE.Vector3(0, 4, 0), // v8.18: Pre-allocated for autopilot _autopilotDir: new THREE.Vector3(), // v8.18: Pre-allocated for autopilot _autopilotDesiredVel: new THREE.Vector3() // v8.18: Pre-allocated for autopilot }; const LANDING_CONFIG = { startAltitude: 60, maxSpeed: 8, safeSpeed: 2.5, gravity: 0.02, // Much slower gravity thrustPower: 0.06, // Gentler thrust manualControl: 0.25, // Slower manual movement fuelConsumption: 0.02, // Slower fuel drain landingPadSize: 18, // Bigger landing pad bounds: 100, biomeColors: { Terra: { sky: 0x87CEEB, ground: 0x3a8c3a, fog: 0x87CEEB }, Desert: { sky: 0xffcc99, ground: 0xc2a060, fog: 0xffcc99 }, Ice: { sky: 0xddeeff, ground: 0xe8f4f8, fog: 0xddeeff }, Volcanic: { sky: 0x330000, ground: 0x2a1a1a, fog: 0x330000 }, Alien: { sky: 0x220044, ground: 0x440066, fog: 0x220044 } } }; function startLandingGame(civ) { // v6.64: Final safety check - don't land on destroyed planets if (!civ || civ.orbital?.destroyed) { showNotification(`${civ?.name || 'Target planet'} no longer exists! Landing aborted.`, 'error'); AudioSystem.error(); return; } // Cleanup any existing landing game if (landingGame.animFrame) { cancelAnimationFrame(landingGame.animFrame); } if (landingGame.renderer) { landingGame.renderer.dispose(); } landingGame.active = true; landingGame.targetCiv = civ; landingGame.isManual = false; landingGame.fuel = 100; landingGame.velocity = new THREE.Vector3(0, -0.1, 0); // Very slow initial descent landingGame.targetPosition = new THREE.Vector3(0, 20, 0); landingGame.lastTime = 0; setMode('landing'); // v8.27: Use setMode() for state validation // Show landing UI const overlay = document.getElementById('landing-overlay'); overlay.style.display = 'block'; document.getElementById('landing-planet-name').textContent = `Landing on ${civ.name} (${civ.biomeName})`; document.getElementById('landing-mode').textContent = 'Autonomous'; document.getElementById('landing-mode-btn').textContent = 'Switch to Manual'; // Get biome colors const biomeColors = LANDING_CONFIG.biomeColors[civ.biome] || LANDING_CONFIG.biomeColors.Terra; // Create separate Three.js scene for landing landingGame.scene = new THREE.Scene(); landingGame.scene.fog = new THREE.Fog(biomeColors.fog, 100, 500); // Isometric camera const container = document.getElementById('landing-scene-container'); const aspect = container.clientWidth / container.clientHeight; const d = 50; landingGame.camera = new THREE.OrthographicCamera( -d * aspect, d * aspect, d, -d, 1, 1000 ); landingGame.camera.position.set(100, 100, 100); landingGame.camera.lookAt(0, 0, 0); // Renderer - v6.87: Mobile optimizations const isMobileLanding = /iphone|ipad|ipod|android/i.test(navigator.userAgent); landingGame.renderer = new THREE.WebGLRenderer({ antialias: !isMobileLanding }); landingGame.renderer.setSize(container.clientWidth, container.clientHeight); landingGame.renderer.setPixelRatio(isMobileLanding ? Math.min(window.devicePixelRatio, 1.5) : Math.min(window.devicePixelRatio, 2)); landingGame.renderer.shadowMap.enabled = !isMobileLanding; landingGame.renderer.shadowMap.type = isMobileLanding ? THREE.BasicShadowMap : THREE.PCFSoftShadowMap; landingGame.renderer.setClearColor(biomeColors.sky); container.innerHTML = ''; container.appendChild(landingGame.renderer.domElement); // Lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); landingGame.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(50, 100, 50); directionalLight.castShadow = true; directionalLight.shadow.camera.left = -100; directionalLight.shadow.camera.right = 100; directionalLight.shadow.camera.top = 100; directionalLight.shadow.camera.bottom = -100; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; landingGame.scene.add(directionalLight); // Ground const groundGeometry = new THREE.PlaneGeometry(300, 300); const groundMaterial = new THREE.MeshLambertMaterial({ color: biomeColors.ground }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; landingGame.scene.add(ground); // Create landing pad createLandingPad(); // Create environment based on biome createLandingEnvironment(civ.biome); // Create ship createLandingShip(); // Start animation loop landingGameLoop(0); // Simple short blip sound instead of ringing tone AudioSystem.click(); showNotification(`Approaching ${civ.name}... Land on the green pad!`, 'info'); } function createLandingPad() { const padGroup = new THREE.Group(); // Main pad const padGeometry = new THREE.CylinderGeometry(LANDING_CONFIG.landingPadSize, LANDING_CONFIG.landingPadSize, 1, 32); const padMaterial = new THREE.MeshLambertMaterial({ color: 0x44ff44 }); const pad = new THREE.Mesh(padGeometry, padMaterial); pad.position.y = 0.5; pad.receiveShadow = true; padGroup.add(pad); // Center marker const markerGeometry = new THREE.CylinderGeometry(3, 3, 0.5, 32); const markerMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff }); const marker = new THREE.Mesh(markerGeometry, markerMaterial); marker.position.y = 1.2; padGroup.add(marker); // Beacon light const beaconGeometry = new THREE.CylinderGeometry(1, 1, 5, 8); const beaconMaterial = new THREE.MeshLambertMaterial({ color: 0x888888 }); const beacon = new THREE.Mesh(beaconGeometry, beaconMaterial); beacon.position.set(LANDING_CONFIG.landingPadSize - 2, 3, 0); beacon.castShadow = true; padGroup.add(beacon); // Beacon light const lightGeometry = new THREE.SphereGeometry(1.5, 16, 16); const lightMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); landingGame.beaconLight = new THREE.Mesh(lightGeometry, lightMaterial); landingGame.beaconLight.position.set(LANDING_CONFIG.landingPadSize - 2, 6, 0); padGroup.add(landingGame.beaconLight); landingGame.landingPad = padGroup; landingGame.scene.add(padGroup); } function createLandingEnvironment(biome) { landingGame.environmentObjects = []; // Add trees/structures based on biome if (biome === 'Terra' || biome === 'Alien') { for (let i = 0; i < 15; i++) { const x = (Math.random() - 0.5) * 250; const z = (Math.random() - 0.5) * 250; if (Math.abs(x) > 30 || Math.abs(z) > 30) { const tree = createLandingTree(biome); tree.position.set(x, 0, z); landingGame.scene.add(tree); landingGame.environmentObjects.push(tree); } } } // Add rocks/obstacles for all biomes for (let i = 0; i < 8; i++) { const x = (Math.random() - 0.5) * 200; const z = (Math.random() - 0.5) * 200; if (Math.abs(x) > 25 || Math.abs(z) > 25) { const rock = createLandingRock(biome); rock.position.set(x, 0, z); landingGame.scene.add(rock); landingGame.environmentObjects.push(rock); } } } function createLandingTree(biome) { const group = new THREE.Group(); // v6.72: Minecraft-style procedural textures // Trunk const trunkGeometry = new THREE.CylinderGeometry(2, 3, 15); const trunkColor = biome === 'Alien' ? 0x8800ff : 0x8B4513; const trunkMaterial = MinecraftTextures.createWoodMaterial(trunkColor, 12345); const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial); trunk.position.y = 7.5; trunk.castShadow = true; group.add(trunk); // Foliage const foliageGeometry = new THREE.SphereGeometry(8, 8, 6); const foliageColor = biome === 'Alien' ? 0xff00ff : 0x228B22; const foliageMaterial = MinecraftTextures.createLeafMaterial(foliageColor, 54321); const foliage = new THREE.Mesh(foliageGeometry, foliageMaterial); foliage.position.y = 18; foliage.castShadow = true; group.add(foliage); return group; } function createLandingRock(biome) { // v6.72: Minecraft-style procedural textures const rockColors = { Terra: 0x888888, Desert: 0xaa5522, Ice: 0xaaccff, Volcanic: 0x333333, Alien: 0x00ffcc }; const height = 5 + Math.random() * 15; const geometry = new THREE.DodecahedronGeometry(3 + Math.random() * 5, 0); const biomeData = BIOMES[biome] || { rock: rockColors[biome] || 0x888888 }; const material = MinecraftTextures.createRockMaterial({ rock: biomeData.rock || rockColors[biome] || 0x888888 }); const rock = new THREE.Mesh(geometry, material); rock.position.y = height / 2; rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI); rock.scale.y = height / 10; rock.castShadow = true; rock.receiveShadow = true; return rock; } function createLandingShip() { const shipGroup = new THREE.Group(); // Body const bodyGeometry = new THREE.BoxGeometry(6, 2, 6); const bodyMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.castShadow = true; shipGroup.add(body); // Cockpit const cockpitGeometry = new THREE.SphereGeometry(2, 16, 16); const cockpitMaterial = new THREE.MeshLambertMaterial({ color: 0x00ffff }); const cockpit = new THREE.Mesh(cockpitGeometry, cockpitMaterial); cockpit.position.y = 1.5; cockpit.scale.y = 0.6; shipGroup.add(cockpit); // Propeller arms and propellers landingGame.propellers = []; const propPositions = [ [-4, 0.5, -4], [4, 0.5, -4], [-4, 0.5, 4], [4, 0.5, 4] ]; propPositions.forEach(pos => { // Arm const armGeometry = new THREE.BoxGeometry(1, 0.5, 1); const armMaterial = new THREE.MeshLambertMaterial({ color: 0x666666 }); const arm = new THREE.Mesh(armGeometry, armMaterial); arm.position.set(pos[0] * 0.6, pos[1], pos[2] * 0.6); shipGroup.add(arm); // Propeller const propGeometry = new THREE.CylinderGeometry(0.2, 0.2, 4); const propMaterial = new THREE.MeshLambertMaterial({ color: 0xaaaaaa }); const propeller = new THREE.Mesh(propGeometry, propMaterial); propeller.rotation.z = Math.PI / 2; propeller.position.set(...pos); propeller.castShadow = true; landingGame.propellers.push(propeller); shipGroup.add(propeller); }); // Engine light landingGame.thrustLight = new THREE.PointLight(0x00ff00, 1, 15); landingGame.thrustLight.position.y = -1; shipGroup.add(landingGame.thrustLight); // Position ship at start shipGroup.position.set( (Math.random() - 0.5) * 40, LANDING_CONFIG.startAltitude, (Math.random() - 0.5) * 40 ); landingGame.ship = shipGroup; landingGame.scene.add(shipGroup); } function landingGameLoop(currentTime) { if (!landingGame.active) return; const deltaTime = (currentTime - landingGame.lastTime) / 1000; landingGame.lastTime = currentTime; if (deltaTime > 0 && deltaTime < 0.1) { updateLandingShip(deltaTime); checkLandingConditions(); updateLandingUI(); } // Rotate propellers // v8.16: forEach-to-for optimization (hot path in animation loop) const propellers = landingGame.propellers; for (let pi = 0, plen = propellers.length; pi < plen; pi++) { propellers[pi].rotation.y += deltaTime * 50; } // Blink beacon const blinkOn = Math.floor(currentTime / 500) % 2 === 0; if (landingGame.beaconLight) { landingGame.beaconLight.material.color.setHex(blinkOn ? 0xff0000 : 0x440000); } // Camera follow // v8.16: Use pre-allocated vector to avoid allocation per frame if (!landingGame._cameraTargetVec) landingGame._cameraTargetVec = new THREE.Vector3(); const cameraTarget = landingGame._cameraTargetVec.set( landingGame.ship.position.x * 0.3, 0, landingGame.ship.position.z * 0.3 ); landingGame.camera.position.x = 100 + cameraTarget.x; landingGame.camera.position.z = 100 + cameraTarget.z; landingGame.camera.lookAt(cameraTarget); landingGame.renderer.render(landingGame.scene, landingGame.camera); landingGame.animFrame = requestAnimationFrame(landingGameLoop); } function updateLandingShip(deltaTime) { const ship = landingGame.ship; // Slow fuel drain landingGame.fuel = Math.max(0, landingGame.fuel - deltaTime * LANDING_CONFIG.fuelConsumption * 3); if (!landingGame.isManual && landingGame.fuel > 0) { // Autonomous flight - navigate to landing pad runLandingAutopilot(deltaTime); } else if (landingGame.isManual && landingGame.fuel > 0) { // Manual controls via keyboard applyManualLandingControls(deltaTime); } // Apply gentle gravity landingGame.velocity.y -= LANDING_CONFIG.gravity * deltaTime * 20; // Apply velocity (slower multiplier) // v7.84: Use pre-allocated temp vector instead of clone() per frame landingGame._tempVelocity.copy(landingGame.velocity).multiplyScalar(deltaTime * 25); ship.position.add(landingGame._tempVelocity); // Tilt based on velocity ship.rotation.z = landingGame.velocity.x * 0.05; ship.rotation.x = -landingGame.velocity.z * 0.05; // Strong damping for smoother movement landingGame.velocity.multiplyScalar(0.96); // Keep within bounds ship.position.clamp( new THREE.Vector3(-LANDING_CONFIG.bounds, 2, -LANDING_CONFIG.bounds), new THREE.Vector3(LANDING_CONFIG.bounds, 100, LANDING_CONFIG.bounds) ); // Update thrust light color based on mode if (landingGame.thrustLight) { landingGame.thrustLight.color.setHex(landingGame.isManual ? 0xff8800 : 0x00ff88); landingGame.thrustLight.intensity = 1 + Math.sin(Date.now() * 0.005) * 0.3; } } function runLandingAutopilot(deltaTime) { const ship = landingGame.ship; // v8.18: Use pre-allocated Vector3 instead of new allocations per frame const padPos = landingGame._autopilotPadPos; // Target slightly above pad (set in landingGame) // Calculate direction to landing pad const direction = landingGame._autopilotDir.subVectors(padPos, ship.position); const horizontalDist = Math.sqrt(direction.x * direction.x + direction.z * direction.z); const verticalDist = ship.position.y; // Desired velocity based on position - MUCH slower and gentler const desiredVelocity = landingGame._autopilotDesiredVel.set(0, 0, 0); // Horizontal movement - very gentle drift toward pad if (horizontalDist > 3) { desiredVelocity.x = direction.x * 0.015; // Very slow horizontal desiredVelocity.z = direction.z * 0.015; } // Vertical movement - very controlled slow descent const slowDescent = -0.15; // Very slow descent speed if (verticalDist > 25) { // High altitude - still slow descent desiredVelocity.y = slowDescent * 1.5; } else if (horizontalDist > 8) { // Not over pad yet - hover and drift desiredVelocity.y = -0.05; } else { // Over pad - very slow final descent desiredVelocity.y = slowDescent * 0.5; } // Very gentle lerp toward desired velocity landingGame.velocity.lerp(desiredVelocity, deltaTime * 0.8); // Counter gravity gently when needed if (landingGame.velocity.y < desiredVelocity.y - 0.05) { landingGame.velocity.y += LANDING_CONFIG.thrustPower * deltaTime * 30; landingGame.fuel -= LANDING_CONFIG.fuelConsumption * 0.5; } } function applyManualLandingControls(deltaTime) { const controlForce = LANDING_CONFIG.manualControl * deltaTime * 60; if (landingKeys['ArrowUp'] || landingKeys['w']) { landingGame.velocity.z -= controlForce; } if (landingKeys['ArrowDown'] || landingKeys['s']) { landingGame.velocity.z += controlForce; } if (landingKeys['ArrowLeft'] || landingKeys['a']) { landingGame.velocity.x -= controlForce; } if (landingKeys['ArrowRight'] || landingKeys['d']) { landingGame.velocity.x += controlForce; } if (landingKeys[' ']) { landingGame.velocity.y += controlForce * 1.5; landingGame.fuel -= LANDING_CONFIG.fuelConsumption * 2; } if (landingKeys['Shift']) { landingGame.velocity.y -= controlForce * 0.5; } } const landingKeys = {}; function handleLandingKeyDown(e) { if (!landingGame.active) return; landingKeys[e.key] = true; if (e.key === 'Escape') { abortLanding(); } if (e.key === 'm' || e.key === 'M') { toggleLandingMode(); } // Auto-switch to manual if keys pressed if (!landingGame.isManual && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ', 'w', 'a', 's', 'd'].includes(e.key)) { landingGame.isManual = true; updateLandingModeUI(); showNotification('MANUAL OVERRIDE - Autopilot disengaged', 'info'); } e.preventDefault(); } function handleLandingKeyUp(e) { landingKeys[e.key] = false; } function toggleLandingMode() { landingGame.isManual = !landingGame.isManual; updateLandingModeUI(); showNotification(landingGame.isManual ? 'MANUAL CONTROL' : 'AUTOPILOT ENGAGED', 'info'); } function updateLandingModeUI() { document.getElementById('landing-mode').textContent = landingGame.isManual ? 'Manual' : 'Autonomous'; document.getElementById('landing-mode-btn').textContent = landingGame.isManual ? 'Switch to Autonomous' : 'Switch to Manual'; } function updateLandingUI() { const ship = landingGame.ship; const altitude = Math.max(0, ship.position.y - 1).toFixed(1); const speed = landingGame.velocity.length().toFixed(1); // v8.18: Removed Vector3 allocation - pad is at origin, just use ship position directly const distance = Math.sqrt( ship.position.x * ship.position.x + ship.position.z * ship.position.z ).toFixed(1); document.getElementById('landing-altitude').textContent = altitude; document.getElementById('landing-speed').textContent = speed; document.getElementById('landing-fuel').textContent = Math.floor(landingGame.fuel); document.getElementById('landing-distance').textContent = distance; } function checkLandingConditions() { const ship = landingGame.ship; const altitude = ship.position.y; const speed = landingGame.velocity.length(); const horizontalDist = Math.sqrt(ship.position.x * ship.position.x + ship.position.z * ship.position.z); // Check if landed if (altitude <= 3) { const onPad = horizontalDist < LANDING_CONFIG.landingPadSize; const slowEnough = speed < LANDING_CONFIG.safeSpeed; if (onPad && slowEnough) { landingSuccess(); } else if (!slowEnough) { landingCrash('Too fast! Reduce speed before landing.'); } else { landingCrash('Missed the landing pad!'); } } // Out of fuel if (landingGame.fuel <= 0 && altitude > 10) { landingCrash('Out of fuel!'); } } function landingSuccess() { landingGame.active = false; cancelAnimationFrame(landingGame.animFrame); const civ = landingGame.targetCiv; const bonusXp = Math.floor(landingGame.fuel * 2); cleanupLandingGame(); showNotification(`Perfect landing on ${civ.name}! +${bonusXp} XP bonus!`, 'success'); AudioSystem.levelUp(); // Grant landing bonus XP Object.keys(gameData.skills).forEach(skill => { addXp(skill, Math.floor(bonusXp / 6)); }); // Track successful landings gameData.statistics.successfulLandings = (gameData.statistics.successfulLandings || 0) + 1; // Now actually enter the world initWorld(civ); } function landingCrash(reason) { landingGame.active = false; cancelAnimationFrame(landingGame.animFrame); cleanupLandingGame(); showNotification(`Crash landing! ${reason}`, 'error'); AudioSystem.error(); // Take damage // v8.26: Guard against undefined gameData.player if (gameData?.player?.hp !== undefined) { gameData.player.hp = Math.max(1, gameData.player.hp - 20); } updateHealthUI(); // Track crashes gameData.statistics.crashLandings = (gameData.statistics.crashLandings || 0) + 1; setMode('galaxy'); // v8.27: Use setMode() for state validation } function abortLanding() { landingGame.active = false; cancelAnimationFrame(landingGame.animFrame); cleanupLandingGame(); setMode('galaxy'); // v8.27: Use setMode() for state validation showNotification('Landing aborted. Returning to orbit.', 'info'); } function cleanupLandingGame() { document.getElementById('landing-overlay').style.display = 'none'; if (landingGame.renderer) { landingGame.renderer.dispose(); const container = document.getElementById('landing-scene-container'); if (container) container.innerHTML = ''; } // Reset keys Object.keys(landingKeys).forEach(k => landingKeys[k] = false); } // Math Utils class SeededRNG { constructor(seed) { this.seed = this.hash(seed); } hash(str) { let h = 0; for(let i=0;i ${newMode}`); mode = newMode; return true; } let activeCiv = null; let raycaster = new THREE.Raycaster(); let mouse = new THREE.Vector2(); let isTouchDevice = 'ontouchstart' in window; // v8.27: Enhanced touch utilities for improved mobile responsiveness const TouchUtils = { // More accurate touch device detection isTouchCapable: ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0), // Gesture recognition state gestures: { startX: 0, startY: 0, startTime: 0, lastTapTime: 0, tapCount: 0 }, // Configurable thresholds thresholds: { swipeDistance: 50, // Minimum distance for swipe swipeTime: 300, // Max time for swipe gesture tapMaxMove: 10, // Max movement to still count as tap doubleTapTime: 300, // Max time between taps for double-tap longPressTime: 500 // Time to trigger long press }, // Start tracking a gesture startGesture(touch) { this.gestures.startX = touch.clientX; this.gestures.startY = touch.clientY; this.gestures.startTime = performance.now(); }, // Detect gesture type from end touch detectGesture(touch) { const dx = touch.clientX - this.gestures.startX; const dy = touch.clientY - this.gestures.startY; const distance = Math.sqrt(dx * dx + dy * dy); const duration = performance.now() - this.gestures.startTime; // Tap detection if (distance < this.thresholds.tapMaxMove && duration < this.thresholds.swipeTime) { const now = performance.now(); if ((now - this.gestures.lastTapTime) < this.thresholds.doubleTapTime) { this.gestures.tapCount++; this.gestures.lastTapTime = now; return { type: 'doubleTap', x: touch.clientX, y: touch.clientY }; } this.gestures.tapCount = 1; this.gestures.lastTapTime = now; return { type: 'tap', x: touch.clientX, y: touch.clientY }; } // Swipe detection if (distance >= this.thresholds.swipeDistance && duration < this.thresholds.swipeTime) { const direction = Math.abs(dx) > Math.abs(dy) ? (dx > 0 ? 'right' : 'left') : (dy > 0 ? 'down' : 'up'); return { type: 'swipe', direction, distance, dx, dy }; } // Drag return { type: 'drag', dx, dy, distance }; }, // Calculate touch velocity for smoother interactions getVelocity(touch, prevTouch, dt) { if (!prevTouch || dt === 0) return { vx: 0, vy: 0 }; return { vx: (touch.clientX - prevTouch.clientX) / dt, vy: (touch.clientY - prevTouch.clientY) / dt }; }, // Convert touch event to normalized coordinates (-1 to 1) normalizeTouch(touch, element) { const rect = element.getBoundingClientRect(); return { x: ((touch.clientX - rect.left) / rect.width) * 2 - 1, y: -((touch.clientY - rect.top) / rect.height) * 2 + 1 }; } }; // Galaxy State let civilizations = []; let galaxyGroup = new THREE.Group(); let selectionRing; let lastTime = 0; let cycle = 0; // Floater pool for performance const floaterPool = []; const MAX_FLOATERS = 20; // RPG State let worldState = { player: null, terrain: [], interactables: [], fishingSpots: [], mobs: [], pois: [], // v4.2: Points of Interest structures: [], // v5.18: Battery chargers and built structures terraformedAreas: [], // v5.18: Flattened terrain zones sun: null, ambient: null, timeOfDay: 0, target: null, interactTarget: null, lastActionTime: 0, // v4.0: Cooldown-based interactions lastPlayerPos: null, // v4.2: For distance tracking // v7.86: Pre-allocated vector for target setting to avoid clone() allocations _targetVec: null // Initialized after THREE.js is available }; // v7.86: Helper to set worldState.target without clone() allocation // Uses copy() on a reusable vector instead of creating new Vector3 each time function setWorldTarget(sourceVec) { if (!sourceVec) { worldState.target = null; return; } if (!worldState._targetVec) { worldState._targetVec = new THREE.Vector3(); } worldState._targetVec.copy(sourceVec); worldState.target = worldState._targetVec; } // v7.86: Helper that adds an offset to target position function setWorldTargetWithOffset(sourceVec, offsetVec) { if (!sourceVec) { worldState.target = null; return; } if (!worldState._targetVec) { worldState._targetVec = new THREE.Vector3(); } worldState._targetVec.copy(sourceVec).add(offsetVec); worldState.target = worldState._targetVec; } // v7.86: Helper to set agent task.targetPosition without clone() allocation // Uses copy() on the pre-allocated _targetVec in taskState function setAgentTarget(task, sourceVec) { if (!sourceVec) { task.targetPosition = null; return; } if (!task._targetVec) { task._targetVec = new THREE.Vector3(); } task._targetVec.copy(sourceVec); task.targetPosition = task._targetVec; } // v7.86: Helper that sets agent target with an offset function setAgentTargetWithOffset(task, sourceVec, offsetVec) { if (!sourceVec) { task.targetPosition = null; return; } if (!task._targetVec) { task._targetVec = new THREE.Vector3(); } task._targetVec.copy(sourceVec).add(offsetVec); task.targetPosition = task._targetVec; } // ============================================ // v9.6: RTS MULTI-SELECT SYSTEM // Starcraft/Warcraft 3/DOTA 2 style selection // Drag-select multiple units, bottom portrait bar // ============================================ const RTSSelection = { // Selected units array selectedUnits: [], // Drag selection state isDragging: false, dragStart: { x: 0, y: 0 }, dragEnd: { x: 0, y: 0 }, dragThreshold: 5, // Minimum pixels to count as drag // 3D selection rings in scene selectionRings: [], // Portrait icons by unit type unitIcons: { player: '🤖', mob: '👾', boss: '💀', creep: '🐛', hostileCreep: '🦂', neutral: '🦎', tower: '🗼', hostileTower: '🔥', spawnPlatform: '🏰', npc: '👤', companion: '🐕', agent: '🤖', structure: '🏗️', interactable: '💎' }, // Get all selectable entities in the world getSelectableEntities() { const entities = []; // Player robot (always first) if (worldState.player) { entities.push({ type: 'player', mesh: worldState.player, name: 'Explorer Robot', hp: gameData.hp, maxHp: gameData.maxHp, faction: 'player' }); } // Mobs - v8.08: forEach to for loop if (worldState.mobs) { for (let i = 0; i < worldState.mobs.length; i++) { const mob = worldState.mobs[i]; if (mob && mob.userData) { entities.push({ type: mob.userData.type === 'boss' ? 'boss' : 'mob', mesh: mob, name: mob.userData.name || 'Creature', hp: mob.userData.hp, maxHp: mob.userData.maxHp, faction: 'hostile' }); } } } // Hostile creeps - v8.08: forEach to for loop if (typeof creepWaveState !== 'undefined' && creepWaveState.creeps) { for (let i = 0; i < creepWaveState.creeps.length; i++) { const creep = creepWaveState.creeps[i]; if (creep && creep.userData) { entities.push({ type: creep.userData.team === 'B' ? 'hostileCreep' : 'creep', mesh: creep, name: creep.userData.name || 'Creep', hp: creep.userData.hp, maxHp: creep.userData.maxHp, faction: creep.userData.team === 'B' ? 'hostile' : 'friendly' }); } } } // Neutral creatures - v8.08: forEach to for loop if (typeof neutralCampState !== 'undefined' && neutralCampState.creatures) { for (let i = 0; i < neutralCampState.creatures.length; i++) { const creature = neutralCampState.creatures[i]; if (creature && creature.userData) { entities.push({ type: 'neutral', mesh: creature, name: creature.userData.name || 'Neutral', hp: creature.userData.hp, maxHp: creature.userData.maxHp, faction: 'neutral' }); } } } // Towers - v8.08: forEach to for loop if (typeof laneSupportState !== 'undefined' && laneSupportState.laneTowers) { for (let i = 0; i < laneSupportState.laneTowers.length; i++) { const tower = laneSupportState.laneTowers[i]; if (tower && tower.mesh) { entities.push({ type: tower.team === 'robot' ? 'tower' : 'hostileTower', mesh: tower.mesh, name: tower.name || 'Tower', hp: tower.hp, maxHp: tower.maxHp, faction: tower.team === 'robot' ? 'friendly' : 'hostile' }); } } } // Spawn platforms - v8.08: forEach to for loop if (typeof creepWaveState !== 'undefined' && creepWaveState.spawnPlatforms) { for (let i = 0; i < creepWaveState.spawnPlatforms.length; i++) { const platform = creepWaveState.spawnPlatforms[i]; if (platform && platform.mesh && platform.active) { entities.push({ type: 'spawnPlatform', mesh: platform.mesh, name: platform.name || 'Spawn Platform', hp: platform.hp, maxHp: platform.maxHp, faction: platform.team === 'B' ? 'hostile' : 'friendly' }); } } } // NPCs/Companions from copilot - v9.9: Use actual companion data if (typeof copilotMesh !== 'undefined' && copilotMesh && gameData.companion && gameData.companion.hp > 0) { entities.push({ type: 'companion', mesh: copilotMesh, name: gameData.companion.name || 'Companion', hp: gameData.companion.hp, maxHp: gameData.companion.maxHp, bond: gameData.companion.bond || 0, generation: gameData.companion.generation || 1, personality: gameData.companion.personality || [], faction: 'friendly' }); } return entities; }, // Check if a point is inside a screen-space box isPointInBox(point, box) { const minX = Math.min(box.x1, box.x2); const maxX = Math.max(box.x1, box.x2); const minY = Math.min(box.y1, box.y2); const maxY = Math.max(box.y1, box.y2); return point.x >= minX && point.x <= maxX && point.y >= minY && point.y <= maxY; }, // Project 3D position to screen coordinates worldToScreen(position) { if (!camera) return null; const vector = position.clone(); vector.project(camera); return { x: (vector.x * 0.5 + 0.5) * window.innerWidth, y: (vector.y * -0.5 + 0.5) * window.innerHeight }; }, // Select entities within drag box selectInBox() { const box = { x1: this.dragStart.x, y1: this.dragStart.y, x2: this.dragEnd.x, y2: this.dragEnd.y }; const entities = this.getSelectableEntities(); const newSelection = []; // v8.08: forEach to for loop for (let i = 0; i < entities.length; i++) { const entity = entities[i]; if (entity.mesh && entity.mesh.position) { const screenPos = this.worldToScreen(entity.mesh.position); if (screenPos && this.isPointInBox(screenPos, box)) { newSelection.push(entity); } } } // If nothing selected, default to player if (newSelection.length === 0 && worldState.player) { const playerEntity = entities.find(e => e.type === 'player'); if (playerEntity) newSelection.push(playerEntity); } this.setSelection(newSelection); }, // Set the current selection setSelection(entities) { this.selectedUnits = entities; this.updateSelectionRings(); this.updatePortraitPanel(); // If single hostile unit selected, also set as interactTarget for auto-attack if (entities.length === 1 && entities[0].faction === 'hostile') { worldState.interactTarget = entities[0].mesh; } }, // Add to current selection (Shift+click) addToSelection(entity) { if (!this.selectedUnits.find(e => e.mesh === entity.mesh)) { this.selectedUnits.push(entity); this.updateSelectionRings(); this.updatePortraitPanel(); } }, // Remove from selection removeFromSelection(entity) { const idx = this.selectedUnits.findIndex(e => e.mesh === entity.mesh); if (idx > -1) { this.selectedUnits.splice(idx, 1); this.updateSelectionRings(); this.updatePortraitPanel(); } }, // Clear selection (but keep player) clearSelection() { this.selectedUnits = []; // Default to player if (worldState.player) { const playerEntity = { type: 'player', mesh: worldState.player, name: 'Explorer Robot', hp: gameData.hp, maxHp: gameData.maxHp, faction: 'player' }; this.selectedUnits = [playerEntity]; } this.updateSelectionRings(); this.updatePortraitPanel(); }, // Create 3D selection ring for a mesh createSelectionRing(mesh, color = 0x00ff88) { const radius = mesh.userData?.radius || 1.5; const ring = new THREE.Mesh( new THREE.RingGeometry(radius * 0.9, radius * 1.1, 32), new THREE.MeshBasicMaterial({ color, side: THREE.DoubleSide, transparent: true, opacity: 0.7 }) ); ring.rotation.x = -Math.PI / 2; ring.position.copy(mesh.position); ring.position.y = 0.2; ring.userData.selectionRing = true; ring.userData.targetMesh = mesh; scene.add(ring); return ring; }, // Update 3D selection rings // v8.17: forEach-to-for loop conversion for selection rings (hot path) updateSelectionRings() { // Remove old rings const oldRings = this.selectionRings; for (let ri = 0, rlen = oldRings.length; ri < rlen; ri++) { if (scene) scene.remove(oldRings[ri]); } this.selectionRings = []; // Create new rings for selected units const units = this.selectedUnits; for (let ui = 0, ulen = units.length; ui < ulen; ui++) { const entity = units[ui]; if (entity.mesh && scene) { const color = entity.faction === 'hostile' ? 0xff4444 : entity.faction === 'neutral' ? 0xffaa00 : entity.faction === 'player' ? 0x00ffff : 0x00ff88; const ring = this.createSelectionRing(entity.mesh, color); this.selectionRings.push(ring); } } }, // Update selection ring positions (call in game loop) // v8.17: forEach-to-for loop conversion for ring positions (hot path) updateRingPositions() { const rings = this.selectionRings; for (let ri = 0, rlen = rings.length; ri < rlen; ri++) { const ring = rings[ri]; if (ring.userData.targetMesh) { ring.position.x = ring.userData.targetMesh.position.x; ring.position.z = ring.userData.targetMesh.position.z; ring.position.y = 0.2; } } }, // Get portrait icon for entity type getIcon(type) { return this.unitIcons[type] || '❓'; }, // Get faction class for styling getFactionClass(faction) { switch (faction) { case 'hostile': return 'hostile'; case 'neutral': return 'neutral'; case 'friendly': return 'friendly'; case 'player': return 'player'; default: return ''; } }, // v9.7: 3D Portrait Renderer System portraitRenderers: [], portraitAnimationFrame: null, // v9.7: Robot voice sounds for selection robotVoices: { select: ['boop', 'beep', 'bip', 'dwee'], damage: ['ow', 'bzzt', 'eek'], ready: ['beep-boop', 'ready', 'online'] }, // v9.7: Play robot voice sound playRobotVoice(type = 'select') { if (!AudioSystem || !AudioSystem.ctx) return; const voices = this.robotVoices[type] || this.robotVoices.select; const voice = voices[Math.floor(Math.random() * voices.length)]; // Generate synthesized robot sounds const ctx = AudioSystem.ctx; const now = ctx.currentTime; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); // Different sound patterns for different voices if (voice === 'boop') { osc.type = 'sine'; osc.frequency.setValueAtTime(800, now); osc.frequency.exponentialRampToValueAtTime(400, now + 0.15); gain.gain.setValueAtTime(0.15, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.15); } else if (voice === 'beep') { osc.type = 'square'; osc.frequency.setValueAtTime(1200, now); osc.frequency.setValueAtTime(1000, now + 0.05); gain.gain.setValueAtTime(0.1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1); } else if (voice === 'bip') { osc.type = 'triangle'; osc.frequency.setValueAtTime(1500, now); gain.gain.setValueAtTime(0.12, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.08); } else if (voice === 'dwee') { osc.type = 'sine'; osc.frequency.setValueAtTime(600, now); osc.frequency.exponentialRampToValueAtTime(1200, now + 0.1); osc.frequency.exponentialRampToValueAtTime(800, now + 0.2); gain.gain.setValueAtTime(0.12, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2); } else if (voice === 'beep-boop') { osc.type = 'square'; osc.frequency.setValueAtTime(1000, now); osc.frequency.setValueAtTime(600, now + 0.1); gain.gain.setValueAtTime(0.1, now); gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2); } osc.start(now); osc.stop(now + 0.3); }, // v9.7: Create a portrait renderer for an entity createPortraitRenderer(entity, container) { const width = 76; const height = 56; // Create canvas const canvas = document.createElement('canvas'); canvas.width = width * 2; // Higher res for quality canvas.height = height * 2; canvas.className = 'portrait-3d-canvas'; canvas.style.width = width + 'px'; canvas.style.height = height + 'px'; // Create dedicated renderer const renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true, antialias: true }); renderer.setSize(width * 2, height * 2); renderer.setClearColor(0x000000, 0); // Create portrait scene const portraitScene = new THREE.Scene(); // Cinematic lighting setup const keyLight = new THREE.DirectionalLight(0xffffff, 1.2); keyLight.position.set(2, 3, 4); portraitScene.add(keyLight); const fillLight = new THREE.DirectionalLight(0x4488ff, 0.5); fillLight.position.set(-2, 1, 2); portraitScene.add(fillLight); const rimLight = new THREE.DirectionalLight(0x00ffff, 0.8); rimLight.position.set(0, 2, -3); portraitScene.add(rimLight); const ambient = new THREE.AmbientLight(0x334455, 0.4); portraitScene.add(ambient); // Create portrait camera - cinematic angle looking up at subject const portraitCamera = new THREE.PerspectiveCamera(35, width / height, 0.1, 100); portraitCamera.position.set(0, 0.3, 2.5); portraitCamera.lookAt(0, 0.5, 0); // Clone or create portrait model let portraitModel; if (entity.mesh) { // Deep clone with materials portraitModel = entity.mesh.clone(true); // Clone materials for each child to ensure they render in separate scene portraitModel.traverse(child => { if (child.isMesh && child.material) { if (Array.isArray(child.material)) { child.material = child.material.map(m => m.clone()); } else { child.material = child.material.clone(); } } }); // Reset transforms portraitModel.position.set(0, 0, 0); portraitModel.rotation.set(0, 0, 0); portraitModel.scale.setScalar(1); // Calculate bounding box to center and scale const box = new THREE.Box3().setFromObject(portraitModel); const size = box.getSize(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3()); // Check for valid bounding box const maxDim = Math.max(size.x, size.y, size.z); if (maxDim > 0 && isFinite(maxDim)) { // Scale to fit in frame const scale = 1.5 / maxDim; portraitModel.scale.setScalar(scale); // Center the model portraitModel.position.sub(center.multiplyScalar(scale)); portraitModel.position.y -= 0.2; // Slight offset down } else { // Fallback scale for empty/invalid bounds portraitModel.scale.setScalar(0.5); } } else { // Fallback sphere for entities without mesh const geo = new THREE.SphereGeometry(0.5, 16, 16); const mat = new THREE.MeshStandardMaterial({ color: entity.faction === 'hostile' ? 0xff4444 : 0x00ffff, metalness: 0.3, roughness: 0.7 }); portraitModel = new THREE.Mesh(geo, mat); } portraitScene.add(portraitModel); // Animation state const portraitData = { renderer, scene: portraitScene, camera: portraitCamera, model: portraitModel, entity, canvas, // Animation state lookTarget: new THREE.Vector3(0, 0, 1), currentLook: new THREE.Vector3(0, 0, 1), idleTime: 0, breathPhase: Math.random() * Math.PI * 2, blinkTimer: Math.random() * 3 + 2, lookTimer: Math.random() * 2 + 1, isReacting: false, reactionTimer: 0, lastHp: entity.hp }; container.insertBefore(canvas, container.firstChild); this.portraitRenderers.push(portraitData); return portraitData; }, // v9.7: Animate all portrait renderers // v8.22: forEach-to-for loop optimization animatePortraits(time) { const dt = 0.016; // ~60fps const renderers = this.portraitRenderers; for (let i = 0, len = renderers.length; i < len; i++) { const portrait = renderers[i]; if (!portrait.renderer || !portrait.scene || !portrait.camera) continue; // Check for damage reaction const currentHp = portrait.entity.hp; if (currentHp < portrait.lastHp) { portrait.isReacting = true; portrait.reactionTimer = 0.3; this.playRobotVoice('damage'); } portrait.lastHp = currentHp; // Reaction animation (shake on damage) if (portrait.isReacting) { portrait.reactionTimer -= dt; const shake = Math.sin(time * 50) * 0.05 * (portrait.reactionTimer / 0.3); portrait.model.position.x = shake; if (portrait.reactionTimer <= 0) { portrait.isReacting = false; portrait.model.position.x = 0; } } // Idle breathing animation portrait.breathPhase += dt * 2; const breathOffset = Math.sin(portrait.breathPhase) * 0.02; portrait.model.position.y = -0.2 + breathOffset; // Random look-around behavior portrait.lookTimer -= dt; if (portrait.lookTimer <= 0) { portrait.lookTimer = Math.random() * 3 + 2; // Pick new random look direction portrait.lookTarget.set( (Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.3 + 0.5, 1 ); } // Smooth look interpolation portrait.currentLook.lerp(portrait.lookTarget, dt * 3); // Apply subtle rotation based on look direction portrait.model.rotation.y = portrait.currentLook.x * 0.3; portrait.model.rotation.x = -portrait.currentLook.y * 0.1 + 0.1; // Render portrait.renderer.render(portrait.scene, portrait.camera); } // Continue animation loop if (renderers.length > 0) { this.portraitAnimationFrame = requestAnimationFrame((t) => this.animatePortraits(t)); } }, // v9.7: Clean up portrait renderers cleanupPortraitRenderers() { if (this.portraitAnimationFrame) { cancelAnimationFrame(this.portraitAnimationFrame); this.portraitAnimationFrame = null; } this.portraitRenderers.forEach(portrait => { if (portrait.renderer) { portrait.renderer.dispose(); } if (portrait.scene) { portrait.scene.traverse(obj => { if (obj.geometry) obj.geometry.dispose(); if (obj.material) { if (Array.isArray(obj.material)) { obj.material.forEach(m => m.dispose()); } else { obj.material.dispose(); } } }); } }); this.portraitRenderers = []; }, // Update the portrait panel UI - v9.7: With 3D rendered portraits updatePortraitPanel() { const panel = document.getElementById('portrait-panel'); const container = document.getElementById('portrait-container'); const header = document.getElementById('portrait-header'); const countBadge = document.getElementById('selection-count'); if (!panel || !container) return; // Show panel in world mode if (mode !== 'world') { panel.style.display = 'none'; this.cleanupPortraitRenderers(); return; } panel.style.display = 'flex'; // Check if selection changed - only rebuild if needed const selectionKey = this.selectedUnits.map(e => e.mesh?.uuid || e.name).join(','); if (this.lastSelectionKey === selectionKey) { // v8.17: forEach-to-for loop conversion for HP bar updates (hot path) const units = this.selectedUnits; for (let ui = 0, ulen = units.length; ui < ulen; ui++) { const entity = units[ui]; const hpFill = container.querySelector(`.portrait-card:nth-child(${ui + 1}) .portrait-hp-fill`); if (hpFill) { const hpPercent = entity.maxHp > 0 ? (entity.hp / entity.maxHp) * 100 : 100; hpFill.style.width = hpPercent + '%'; hpFill.className = 'portrait-hp-fill ' + (hpPercent > 60 ? 'high' : hpPercent > 30 ? 'medium' : ''); } } return; } this.lastSelectionKey = selectionKey; // Cleanup old renderers this.cleanupPortraitRenderers(); container.innerHTML = ''; // Update header const count = this.selectedUnits.length; if (count === 0) { header.textContent = 'No Selection'; } else if (count === 1) { header.textContent = this.selectedUnits[0].name; } else { header.textContent = `${count} Units Selected`; } // Update count badge if (count > 1) { countBadge.textContent = count; countBadge.classList.add('active'); } else { countBadge.classList.remove('active'); } // Create portrait cards with 3D renderers this.selectedUnits.forEach((entity, idx) => { const card = document.createElement('div'); // v9.9: Add special 'companion' class for companion entities const typeClass = entity.type === 'companion' ? 'companion' : this.getFactionClass(entity.faction); card.className = `portrait-card ${typeClass}`; if (idx === 0) card.classList.add('selected'); const hpPercent = entity.maxHp > 0 ? (entity.hp / entity.maxHp) * 100 : 100; const hpClass = hpPercent > 60 ? 'high' : hpPercent > 30 ? 'medium' : ''; // v9.9: Companion-specific extra info let extraInfo = ''; if (entity.type === 'companion') { const bondLevel = entity.bond || 0; const bondPercent = Math.min(100, bondLevel); const personality = entity.personality && entity.personality.length > 0 ? entity.personality[0] : 'Loyal'; const generation = entity.generation || 1; extraInfo = `
Bond
${personality} Gen ${generation}
`; } // Create card structure (3D canvas will be prepended) card.innerHTML = `
${entity.name}
${extraInfo}
`; container.appendChild(card); // Create 3D portrait renderer this.createPortraitRenderer(entity, card); // Click to select only this unit card.addEventListener('click', (e) => { e.stopPropagation(); // Play robot voice on selection this.playRobotVoice('select'); if (e.shiftKey) { this.removeFromSelection(entity); } else { this.setSelection([entity]); if (entity.faction === 'hostile') { worldState.interactTarget = entity.mesh; } } }); }); // Start portrait animation loop if (this.selectedUnits.length > 0 && !this.portraitAnimationFrame) { this.animatePortraits(performance.now()); } // If no units, show player by default if (this.selectedUnits.length === 0 && worldState.player) { this.clearSelection(); } }, // Initialize the system init() { // Default to player selected this.clearSelection(); // v9.8: Removed ready beep - was jarring on world load console.log('🎯 RTSSelection: Multi-select system with 3D portraits initialized'); } }; // v5.18: Robot Energy System // v12.16: BATTERY RANGE TETHER - Battery determines exploration radius from landing site // v12.17: UNIFIED BATTERY SYSTEM - HP + Power = Total Battery (inspired by Metroid energy tanks) // v7.33: REBALANCED - Generous exploration, minimal annoyance, establishes mechanic without restricting play let robotEnergy = { current: 100, max: 100, drainRate: 0.005, // v7.33: REDUCED from 0.02 - very slow drain, not a nuisance chargeRate: 8, // v7.33: INCREASED from 5 - faster recharge when at base lowEnergyThreshold: 15, // v7.33: Lower threshold - less nagging isCharging: false, // v12.16: Range Tether System - battery = exploration radius // v7.33: REBALANCED for generous exploration rangePerEnergy: 4.0, // v7.33: INCREASED from 1.5 - each energy = 4 units (100 energy = 400 unit radius!) baseRange: 80, // v7.33: INCREASED from 30 - large safe zone near ship maxBonusRange: 0, // From upgrades/items origin: null, // Landing position (set when entering world) boundaryWarningZone: 0.94, // v7.33: INCREASED from 0.85 - only warn at 94% (very close to edge) graceZone: 0.5, // v7.33: NEW - No drain within 50% of range (casual exploration is FREE) // v12.17: UNIFIED BATTERY - HP and Power as one resource // Battery splits into two logical segments: // - Structural Integrity (HP): Damage drains this, robot dies at 0 // - Power Reserve: Abilities and movement drain this unifiedMode: true, // Enable unified HP/Power battery structuralRatio: 0.6, // 60% of battery = HP pool (60 of 100) powerRatio: 0.4, // 40% of battery = Power pool (40 of 100) structuralDamage: 0, // Accumulated structural damage powerDrain: 0, // Accumulated power usage criticalThreshold: 0.2, // Critical state below 20% structural lowPowerThreshold: 0.25, // Low power warning at 25% power regenRate: 1.5, // v7.33: INCREASED from 0.5 - faster power regen (1.5/sec) lastCombatTime: 0, // Track combat for regen combatRegenDelay: 3000 // v7.33: REDUCED from 5000 - 3 sec out of combat before power regen }; // v12.16: BATTERY RANGE TETHER SYSTEM // The robot's battery acts as an exploration boundary - further from ship = more energy drain // v7.33: REBALANCED - Generous range, minimal warnings, establishes mechanic without restricting play const BatteryRangeSystem = { boundaryRing: null, // v7.33: warningRing removed - single subtle indicator is enough dangerPulse: 0, lastWarningTime: 0, isAtBoundary: false, returnArrow: null, // Calculate max exploration range based on current energy getMaxRange() { const baseRange = robotEnergy.baseRange; const energyRange = robotEnergy.current * robotEnergy.rangePerEnergy; const bonusRange = robotEnergy.maxBonusRange; return baseRange + energyRange + bonusRange; }, // Get distance from landing origin (squared) - v8.08: avoid sqrt when possible getDistanceFromOriginSq() { if (!robotEnergy.origin || !worldState.player) return 0; const dx = worldState.player.position.x - robotEnergy.origin.x; const dz = worldState.player.position.z - robotEnergy.origin.z; return dx * dx + dz * dz; }, // Get distance from landing origin - only call when actual distance needed getDistanceFromOrigin() { return Math.sqrt(this.getDistanceFromOriginSq()); }, // Get percentage of range used (0-1+) - v8.08: uses squared math internally getRangeUsage() { const maxRange = this.getMaxRange(); if (maxRange <= 0) return 1; const maxRangeSq = maxRange * maxRange; const distSq = this.getDistanceFromOriginSq(); // sqrt(distSq) / maxRange = sqrt(distSq / maxRangeSq) return Math.sqrt(distSq / maxRangeSq); }, // Check if a target position is within range - v8.08: uses squared distance isPositionInRange(x, z) { if (!robotEnergy.origin) return true; const dx = x - robotEnergy.origin.x; const dz = z - robotEnergy.origin.z; const distSq = dx * dx + dz * dz; const maxRange = this.getMaxRange(); return distSq <= maxRange * maxRange; }, // Set origin when landing on planet setOrigin(x, z) { robotEnergy.origin = { x, z }; this.createBoundaryVisuals(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`🔋 Battery Range: Origin set at (${x.toFixed(1)}, ${z.toFixed(1)}), max range: ${this.getMaxRange().toFixed(0)} units`); }, // Create visual boundary indicators // v7.33: Made much more subtle - visuals are hints, not warnings createBoundaryVisuals() { if (typeof THREE === 'undefined' || typeof scene === 'undefined') return; // Remove old visuals this.removeBoundaryVisuals(); // Outer boundary ring (hard limit) - v7.33: starts invisible, fades in when approaching const maxRange = this.getMaxRange(); const ringGeo = new THREE.RingGeometry(maxRange - 1, maxRange + 1, 64); const ringMat = new THREE.MeshBasicMaterial({ color: 0xffaa44, // v7.33: Softer orange, not alarming red transparent: true, opacity: 0, // v7.33: Starts invisible - fades in based on proximity side: THREE.DoubleSide, depthWrite: false }); this.boundaryRing = new THREE.Mesh(ringGeo, ringMat); this.boundaryRing.rotation.x = -Math.PI / 2; this.boundaryRing.position.set(robotEnergy.origin.x, 0.5, robotEnergy.origin.z); this.boundaryRing.name = 'batteryBoundaryRing'; scene.add(this.boundaryRing); // v7.33: Warning ring removed - we only need one subtle indicator // The boundary ring now handles all visual feedback with gradual opacity // Return arrow indicator (hidden initially) const arrowShape = new THREE.Shape(); arrowShape.moveTo(0, 2); arrowShape.lineTo(1, 0); arrowShape.lineTo(0.3, 0); arrowShape.lineTo(0.3, -1.5); arrowShape.lineTo(-0.3, -1.5); arrowShape.lineTo(-0.3, 0); arrowShape.lineTo(-1, 0); arrowShape.lineTo(0, 2); const arrowGeo = new THREE.ShapeGeometry(arrowShape); const arrowMat = new THREE.MeshBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0, side: THREE.DoubleSide, depthWrite: false }); this.returnArrow = new THREE.Mesh(arrowGeo, arrowMat); this.returnArrow.rotation.x = -Math.PI / 2; this.returnArrow.scale.set(3, 3, 3); this.returnArrow.name = 'batteryReturnArrow'; scene.add(this.returnArrow); }, removeBoundaryVisuals() { if (typeof scene === 'undefined') return; // v7.33: warningRing removed from system [this.boundaryRing, this.returnArrow].forEach(mesh => { if (mesh) { scene.remove(mesh); if (mesh.geometry) mesh.geometry.dispose(); if (mesh.material) mesh.material.dispose(); } }); this.boundaryRing = null; this.returnArrow = null; }, // Update boundary visuals based on current energy // v7.33: Simplified - only boundary ring, no warning ring updateBoundaryVisuals() { if (!robotEnergy.origin || !this.boundaryRing) return; const maxRange = this.getMaxRange(); // Update boundary ring size if (this.boundaryRing.geometry) this.boundaryRing.geometry.dispose(); this.boundaryRing.geometry = new THREE.RingGeometry(maxRange - 1, maxRange + 1, 64); }, // Main update function - call each frame // v7.33: REBALANCED - Much less intrusive, establishes mechanic without annoying players update(dt, time) { if (!robotEnergy.origin || mode !== 'world') return; const usage = this.getRangeUsage(); const dist = this.getDistanceFromOrigin(); const maxRange = this.getMaxRange(); const graceZone = robotEnergy.graceZone || 0.5; // v7.33: No drain within grace zone // Update danger pulse animation (slower, less frantic) this.dangerPulse = (this.dangerPulse + dt * 1.5) % (Math.PI * 2); // v7.33: Boundary ring only visible when beyond 70% of range (less visual clutter) if (this.boundaryRing) { if (usage < 0.7) { // Hide ring when exploring casually this.boundaryRing.material.opacity = 0; } else if (usage < 0.9) { // Subtle hint when getting further out this.boundaryRing.material.opacity = (usage - 0.7) * 0.3; this.boundaryRing.material.color.setHex(0xffaa44); // Soft orange } else if (usage < 1.0) { // More visible near edge const pulseIntensity = Math.sin(this.dangerPulse) * 0.1; this.boundaryRing.material.opacity = 0.2 + pulseIntensity; this.boundaryRing.material.color.setHex(0xff6644); } else { // At boundary - visible but not overwhelming this.boundaryRing.material.opacity = 0.35 + Math.sin(this.dangerPulse) * 0.15; this.boundaryRing.material.color.setHex(0xff4422); } } // v7.33: Warning zone feedback - MUCH less frequent, friendlier tone if (usage >= robotEnergy.boundaryWarningZone && usage < 1.0) { // Only warn once every 12 seconds, with friendlier message if (time - this.lastWarningTime > 12000) { const percentLeft = Math.round((1 - usage) * 100); showNotification(`🔋 Approaching range limit (${percentLeft}% remaining)`, 'info'); // v7.33: No error sound - just info this.lastWarningTime = time; } } // At or beyond boundary this.isAtBoundary = usage >= 1.0; if (this.isAtBoundary) { // Show return arrow pointing to origin (helpful, not punishing) if (this.returnArrow && worldState.player) { const toOrigin = Math.atan2( robotEnergy.origin.x - worldState.player.position.x, robotEnergy.origin.z - worldState.player.position.z ); this.returnArrow.position.set( worldState.player.position.x, worldState.player.position.y + 3, worldState.player.position.z ); this.returnArrow.rotation.z = toOrigin; this.returnArrow.material.opacity = 0.5 + Math.sin(this.dangerPulse) * 0.2; } // v7.33: Boundary warning - less frequent (every 8 seconds), friendlier if (time - this.lastWarningTime > 8000) { showNotification('🧭 You\'ve reached the exploration limit. Head back when ready!', 'warning'); // v7.33: Gentle audio cue only once if (typeof AudioSystem !== 'undefined' && time - this.lastWarningTime > 15000) { AudioSystem.error(); } this.lastWarningTime = time; } } else { // Hide return arrow when not at boundary if (this.returnArrow) { this.returnArrow.material.opacity = Math.max(0, this.returnArrow.material.opacity - dt * 2); } } // v7.33: GRACE ZONE - No energy drain within first 50% of range (casual exploration is FREE!) // Only drain energy when beyond grace zone, and at a very gentle rate if (!robotEnergy.isCharging && usage > graceZone) { // Calculate how far into the "drain zone" player is (0 at grace boundary, 1 at max range) const drainZoneUsage = (usage - graceZone) / (1 - graceZone); // v7.33: Much gentler drain - scales with distance but stays low const extraDrain = drainZoneUsage * drainZoneUsage * robotEnergy.drainRate * 0.5; robotEnergy.current = Math.max(0, robotEnergy.current - extraDrain * dt); } }, // Check if movement to target is allowed (for tryMovePlayer) // v12.18: Now supports infinite exploration mode bypass // v12.19: Removed invisible wall - entire map is now accessible // When power runs out, player is teleported back to well (origin) instead of blocked canMoveTo(targetX, targetZ) { // v12.19: Always allow movement - no invisible walls // Power depletion handling moved to update() which teleports back to origin return { allowed: true }; }, // Upgrade range (from items, skills, battery upgrades) addBonusRange(amount) { robotEnergy.maxBonusRange += amount; this.updateBoundaryVisuals(); showNotification(`🔋 RANGE UPGRADED: +${amount} exploration radius!`, 'success'); }, // Reset when leaving planet reset() { this.removeBoundaryVisuals(); robotEnergy.origin = null; this.isAtBoundary = false; this.lastWarningTime = 0; } }; // ============================================ // v12.17: UNIFIED BATTERY SYSTEM // HP and Power/Mana as segments of total battery // Inspired by Metroid energy tanks - one resource for everything // ============================================ const UnifiedBatterySystem = { // UI elements batteryUI: null, structuralBar: null, powerBar: null, batteryText: null, // Visual state lastDamageFlash: 0, lastPowerFlash: 0, criticalPulse: 0, lowPowerPulse: 0, // === CORE CALCULATIONS === // Get total structural capacity (HP pool) getStructuralCapacity() { return Math.floor(robotEnergy.max * robotEnergy.structuralRatio); }, // Get total power capacity getPowerCapacity() { return Math.floor(robotEnergy.max * robotEnergy.powerRatio); }, // Get current structural HP (HP pool minus damage taken) getStructuralHP() { const capacity = this.getStructuralCapacity(); return Math.max(0, capacity - robotEnergy.structuralDamage); }, // Get structural HP as percentage (0-1) getStructuralPercent() { const capacity = this.getStructuralCapacity(); if (capacity <= 0) return 0; return this.getStructuralHP() / capacity; }, // Get current power reserve getPowerReserve() { const capacity = this.getPowerCapacity(); return Math.max(0, capacity - robotEnergy.powerDrain); }, // Get power reserve as percentage (0-1) getPowerPercent() { const capacity = this.getPowerCapacity(); if (capacity <= 0) return 0; return this.getPowerReserve() / capacity; }, // Get effective battery level (structural + power remaining) getEffectiveBattery() { return this.getStructuralHP() + this.getPowerReserve(); }, // Get effective battery percentage getEffectiveBatteryPercent() { return this.getEffectiveBattery() / robotEnergy.max; }, // === DAMAGE AND DRAIN === // Apply damage to structural portion (called from damagePlayer) applyStructuralDamage(amount) { if (!robotEnergy.unifiedMode) return amount; const beforeHP = this.getStructuralHP(); robotEnergy.structuralDamage += amount; const afterHP = this.getStructuralHP(); // Update effective battery robotEnergy.current = this.getEffectiveBattery(); // Track combat time for regen delay robotEnergy.lastCombatTime = performance.now(); // Flash effect this.lastDamageFlash = performance.now(); // Check critical state if (this.getStructuralPercent() <= robotEnergy.criticalThreshold) { this.triggerCriticalWarning(); } return beforeHP - afterHP; // Actual damage applied }, // Drain power for abilities/actions drainPower(amount) { if (!robotEnergy.unifiedMode) return true; const currentPower = this.getPowerReserve(); if (currentPower < amount) { // Not enough power! this.triggerLowPowerWarning(); return false; } robotEnergy.powerDrain += amount; robotEnergy.current = this.getEffectiveBattery(); // Flash effect this.lastPowerFlash = performance.now(); // Check low power if (this.getPowerPercent() <= robotEnergy.lowPowerThreshold) { this.triggerLowPowerWarning(); } return true; }, // Check if has enough power hasPower(amount) { if (!robotEnergy.unifiedMode) return true; return this.getPowerReserve() >= amount; }, // Heal structural damage healStructural(amount) { robotEnergy.structuralDamage = Math.max(0, robotEnergy.structuralDamage - amount); robotEnergy.current = this.getEffectiveBattery(); }, // Restore power restorePower(amount) { robotEnergy.powerDrain = Math.max(0, robotEnergy.powerDrain - amount); robotEnergy.current = this.getEffectiveBattery(); }, // Full battery recharge fullRecharge() { robotEnergy.structuralDamage = 0; robotEnergy.powerDrain = 0; robotEnergy.current = robotEnergy.max; }, // === WARNINGS === triggerCriticalWarning() { const now = performance.now(); if (now - this.lastCriticalWarning < 3000) return; this.lastCriticalWarning = now; showNotification('⚠️ CRITICAL: Structural integrity failing!', 'warning'); if (typeof AudioSystem !== 'undefined' && AudioSystem.alarm) { AudioSystem.alarm(); } }, triggerLowPowerWarning() { const now = performance.now(); if (now - this.lastLowPowerWarning < 5000) return; this.lastLowPowerWarning = now; showNotification('🔋 LOW POWER: Abilities restricted!', 'warning'); }, lastCriticalWarning: 0, lastLowPowerWarning: 0, // === UPDATE LOOP === update(dt) { if (!robotEnergy.unifiedMode) return; const now = performance.now(); // v12.19: POWER SAP - Regenerate power when near friendly creeps or towers const proxRegenRate = this.calculateProximityRegen(); if (proxRegenRate > 0 && robotEnergy.powerDrain > 0) { robotEnergy.powerDrain = Math.max(0, robotEnergy.powerDrain - proxRegenRate * dt); robotEnergy.current = this.getEffectiveBattery(); } // Power regeneration (only out of combat) - base regen if (now - robotEnergy.lastCombatTime > robotEnergy.combatRegenDelay) { if (robotEnergy.powerDrain > 0) { robotEnergy.powerDrain = Math.max(0, robotEnergy.powerDrain - robotEnergy.regenRate * dt); robotEnergy.current = this.getEffectiveBattery(); } } // v12.19: POWER DEPLETION TELEPORT - When power runs out, return to well if (this.getPowerReserve() <= 0 && !this._teleportingToWell) { this.teleportToWell(); } // Visual pulses if (this.getStructuralPercent() <= robotEnergy.criticalThreshold) { this.criticalPulse = (this.criticalPulse + dt * 3) % (Math.PI * 2); } if (this.getPowerPercent() <= robotEnergy.lowPowerThreshold) { this.lowPowerPulse = (this.lowPowerPulse + dt * 2) % (Math.PI * 2); } // Sync with gameData.player.hp for compatibility this.syncWithLegacyHP(); // Update UI this.updateUI(); }, // v12.19: Calculate power regeneration from nearby friendly units // The closer you are to friendly creeps/towers, the faster you regen _lastProxRegenNotify: 0, calculateProximityRegen() { if (typeof worldState === 'undefined' || !worldState.player) return 0; const playerPos = worldState.player.position; let totalRegen = 0; let nearbyFriendlies = 0; const maxRange = 25; // Units within 25 units contribute to regen const baseRegenPerUnit = 2.0; // Base regen per nearby friendly unit // Check friendly creeps (team 'A' = explorer team) // v7.80: distanceToSquared optimization - avoid sqrt in hot loop // v8.08: forEach to for loop const maxRangeSq = maxRange * maxRange; if (typeof creepWaveState !== 'undefined' && creepWaveState.creeps) { for (let i = 0; i < creepWaveState.creeps.length; i++) { const creep = creepWaveState.creeps[i]; if (!creep || !creep.userData || !creep.position) continue; if (creep.userData.team !== 'A') continue; // Only friendly creeps const distSq = playerPos.distanceToSquared(creep.position); if (distSq < maxRangeSq) { // Closer = more regen (inverse distance scaling) const dist = Math.sqrt(distSq); // v7.80: Only sqrt when inside range const distFactor = 1 - (dist / maxRange); totalRegen += baseRegenPerUnit * distFactor; nearbyFriendlies++; } } } // Check friendly towers (team 'robot') // v7.80: distanceToSquared optimization // v8.08: forEach to for loop const towerRangeSq = (maxRange * 1.5) * (maxRange * 1.5); if (typeof laneSupportState !== 'undefined' && laneSupportState.laneTowers) { for (let i = 0; i < laneSupportState.laneTowers.length; i++) { const tower = laneSupportState.laneTowers[i]; if (!tower || !tower.active || !tower.mesh) continue; if (tower.team !== 'robot') continue; // Only friendly towers const distSq = playerPos.distanceToSquared(tower.mesh.position); if (distSq < towerRangeSq) { // Towers have larger range const dist = Math.sqrt(distSq); // v7.80: Only sqrt when inside range const distFactor = 1 - (dist / (maxRange * 1.5)); totalRegen += baseRegenPerUnit * 2 * distFactor; // Towers give 2x regen nearbyFriendlies++; } } } // Show notification when gaining power from allies (throttled) const now = performance.now(); if (totalRegen > 0 && now - this._lastProxRegenNotify > 5000 && robotEnergy.powerDrain > 0) { this._lastProxRegenNotify = now; showNotification(`⚡ POWER SAP: Recharging from ${nearbyFriendlies} nearby allies!`, 'info'); } return totalRegen; }, // v12.19: Teleport player back to well (origin) when power depleted _teleportingToWell: false, teleportToWell() { if (!robotEnergy.origin || !worldState.player) return; this._teleportingToWell = true; // Show dramatic notification showNotification('⚡ POWER DEPLETED! Emergency recall to well...', 'warning'); if (typeof AudioSystem !== 'undefined' && AudioSystem.alarm) { AudioSystem.alarm(); } // Screen effect if (typeof screenShake === 'function') screenShake(0.5); if (typeof flashDamageOverlay === 'function') flashDamageOverlay(); // Teleport to origin (the "well") worldState.player.position.set(robotEnergy.origin.x, 10, robotEnergy.origin.z); // Restore power (but not structural HP - that's like death penalty) robotEnergy.powerDrain = 0; robotEnergy.current = this.getEffectiveBattery(); // Clear targets worldState.target = null; worldState.interactTarget = null; // Visual effect at destination if (typeof particles !== 'undefined') { particles.emit(worldState.player.position, 30, 0x00ffff, { spread: 5, lifetime: 1000 }); } if (typeof spawnFloater === 'function') { spawnFloater(worldState.player.position, '🔋 RECHARGED!', '#00ffff'); } // Reset flag after brief delay setTimeout(() => { this._teleportingToWell = false; }, 1000); console.log('⚡ Power depleted - teleported back to well at', robotEnergy.origin); }, // === LEGACY SYNC === // Sync unified battery with gameData.player.hp for compatibility syncWithLegacyHP() { if (!robotEnergy.unifiedMode) return; if (typeof gameData === 'undefined' || !gameData.player) return; // Map structural HP to gameData.player.hp const structuralPercent = this.getStructuralPercent(); gameData.player.hp = Math.floor(structuralPercent * gameData.player.maxHp); }, // === UI === createUI() { // Check if already exists if (document.getElementById('unified-battery-container')) return; const container = document.createElement('div'); container.id = 'unified-battery-container'; container.style.cssText = ` position: fixed; top: 80px; left: 20px; width: 220px; z-index: 1000; font-family: 'Rajdhani', 'Orbitron', monospace; pointer-events: none; `; container.innerHTML = `
🔋 UNIFIED BATTERY 100%
60 HP | 40 PWR
❤️ STRUCTURAL ⚡ POWER
`; document.body.appendChild(container); this.batteryUI = container; this.structuralBar = document.getElementById('unified-structural-bar'); this.powerBar = document.getElementById('unified-power-bar'); this.batteryText = document.getElementById('unified-battery-text'); this.batteryPercent = document.getElementById('unified-battery-percent'); this.batteryStatus = document.getElementById('unified-battery-status'); }, updateUI() { if (!this.batteryUI) this.createUI(); if (!robotEnergy.unifiedMode) { if (this.batteryUI) this.batteryUI.style.display = 'none'; return; } this.batteryUI.style.display = 'block'; const structHP = this.getStructuralHP(); const structCap = this.getStructuralCapacity(); const structPct = this.getStructuralPercent(); const powerRes = this.getPowerReserve(); const powerCap = this.getPowerCapacity(); const powerPct = this.getPowerPercent(); const totalPct = this.getEffectiveBatteryPercent(); // Update bars if (this.structuralBar) { const structWidth = structPct * robotEnergy.structuralRatio * 100; this.structuralBar.style.width = structWidth + '%'; // Critical pulse if (structPct <= robotEnergy.criticalThreshold) { const pulse = 0.5 + 0.5 * Math.sin(this.criticalPulse); this.structuralBar.style.boxShadow = `0 0 ${10 + pulse * 15}px rgba(255,0,0,${0.5 + pulse * 0.5})`; this.structuralBar.style.background = `linear-gradient(90deg, #ff0000 0%, #ff3300 100%)`; } else { this.structuralBar.style.background = `linear-gradient(90deg, #ff3344 0%, #ff6644 100%)`; this.structuralBar.style.boxShadow = '0 0 10px rgba(255,50,50,0.5)'; } } if (this.powerBar) { const powerWidth = powerPct * robotEnergy.powerRatio * 100; this.powerBar.style.width = powerWidth + '%'; // Low power pulse if (powerPct <= robotEnergy.lowPowerThreshold) { const pulse = 0.5 + 0.5 * Math.sin(this.lowPowerPulse); this.powerBar.style.opacity = 0.5 + pulse * 0.5; } else { this.powerBar.style.opacity = 1; } } // Update text if (this.batteryText) { this.batteryText.textContent = `${Math.floor(structHP)} HP | ${Math.floor(powerRes)} PWR`; } if (this.batteryPercent) { this.batteryPercent.textContent = Math.floor(totalPct * 100) + '%'; // Color based on state if (structPct <= robotEnergy.criticalThreshold) { this.batteryPercent.style.color = '#ff4444'; } else if (totalPct <= 0.5) { this.batteryPercent.style.color = '#ffaa00'; } else { this.batteryPercent.style.color = '#00ff88'; } } // Status text if (this.batteryStatus) { let status = ''; if (structPct <= robotEnergy.criticalThreshold) { status = '⚠️ CRITICAL DAMAGE'; } else if (powerPct <= robotEnergy.lowPowerThreshold) { status = '🔋 LOW POWER'; } else if (robotEnergy.isCharging) { status = '⚡ CHARGING'; } else if (performance.now() - robotEnergy.lastCombatTime < robotEnergy.combatRegenDelay) { status = '⚔️ COMBAT MODE'; } this.batteryStatus.textContent = status; this.batteryStatus.style.color = structPct <= robotEnergy.criticalThreshold ? '#ff4444' : '#88ff88'; } }, // Hide UI when not on planet hideUI() { if (this.batteryUI) { this.batteryUI.style.display = 'none'; } }, // Reset for new planet reset() { robotEnergy.structuralDamage = 0; robotEnergy.powerDrain = 0; robotEnergy.current = robotEnergy.max; robotEnergy.lastCombatTime = 0; } }; // ============================================ // v12.17: BATTERY CORE SYSTEM - Permanent Progression // NEVER resets even on prestige - eternal growth // ============================================ const BatteryCoreSystem = { getXPForLevel(level) { return Math.floor(100 * Math.pow(1.15, level - 1)); }, CAPACITY_PER_LEVEL: 2, EFFICIENCY_PER_LEVEL: 0.5, REGEN_PER_LEVEL: 0.02, MILESTONES: { 5: { name: 'Energy Initiate', capacityBonus: 10, perk: 'Power costs -5%' }, 10: { name: 'Power Adept', capacityBonus: 25, perk: 'Structural +10%' }, 25: { name: 'Core Specialist', capacityBonus: 50, perk: 'Regen doubled' }, 50: { name: 'Energy Master', capacityBonus: 100, perk: 'Critical -5%' }, 100: { name: 'BATTERY ASCENDANT', capacityBonus: 200, perk: 'All bonuses 2x' } }, getCore() { if (typeof gameData === 'undefined') return this.getDefaultCore(); if (!gameData.batteryCore) gameData.batteryCore = this.getDefaultCore(); return gameData.batteryCore; }, getDefaultCore() { return { level: 1, xp: 0, totalXP: 0, capacityBonus: 0, efficiencyBonus: 0, regenBonus: 0, milestones: { level5: false, level10: false, level25: false, level50: false, level100: false } }; }, getTotalCapacityBonus() { const core = this.getCore(); let bonus = core.level * this.CAPACITY_PER_LEVEL; for (const [lvl, m] of Object.entries(this.MILESTONES)) { if (core.level >= parseInt(lvl)) bonus += m.capacityBonus; } if (core.milestones.level100) bonus *= 2; return bonus; }, getEfficiencyMultiplier() { const core = this.getCore(); let eff = 1.0 - (core.level * this.EFFICIENCY_PER_LEVEL / 100); if (core.milestones.level5) eff -= 0.05; if (core.milestones.level100) eff = 1.0 - ((1.0 - eff) * 2); return Math.max(0.5, eff); }, getRegenBonus() { const core = this.getCore(); let regen = core.level * this.REGEN_PER_LEVEL; if (core.milestones.level25) regen *= 2; if (core.milestones.level100) regen *= 2; return regen; }, getStructuralMultiplier() { const core = this.getCore(); let mult = 1.0; if (core.milestones.level10) mult += 0.10; if (core.milestones.level100) mult += 0.10; return mult; }, awardXP(amount, source) { if (typeof gameData === 'undefined') return; const core = this.getCore(); const prestigeMult = gameData.prestige?.bonuses?.xpMultiplier || 1.0; const xpGained = Math.floor(amount * prestigeMult); core.xp += xpGained; core.totalXP += xpGained; this.checkLevelUp(); if (xpGained >= 5 && worldState.player) spawnFloater(worldState.player.position, '⚡+' + xpGained + ' Core', '#00ffaa'); }, checkLevelUp() { const core = this.getCore(); let leveledUp = false; while (core.xp >= this.getXPForLevel(core.level)) { core.xp -= this.getXPForLevel(core.level); core.level++; leveledUp = true; core.capacityBonus = this.getTotalCapacityBonus(); core.efficiencyBonus = (1.0 - this.getEfficiencyMultiplier()) * 100; core.regenBonus = this.getRegenBonus(); this.checkMilestones(); } if (leveledUp) { this.onLevelUp(core.level); this.applyToRobotEnergy(); saveGameData(); } return leveledUp; }, checkMilestones() { const core = this.getCore(); for (const [lvl, m] of Object.entries(this.MILESTONES)) { const key = 'level' + lvl; if (core.level >= parseInt(lvl) && !core.milestones[key]) { core.milestones[key] = true; this.onMilestoneUnlock(parseInt(lvl), m); } } }, onLevelUp(newLevel) { showNotification('🔋 BATTERY CORE LV' + newLevel + '! (+' + this.getTotalCapacityBonus() + ' capacity)', 'success'); AudioSystem.levelUp(); if (worldState.player) { if (typeof particles !== 'undefined' && particles) particles.emit(worldState.player.position, 30, 0x00ffaa, { spread: 5, lifetime: 1500 }); spawnFloater(worldState.player.position, '⚡ CORE LV' + newLevel + '!', '#00ffaa'); } screenShake(0.3); }, onMilestoneUnlock(level, milestone) { showNotification('🏆 ' + milestone.name + '! ' + milestone.perk, 'success'); if (worldState.player && typeof particles !== 'undefined' && particles) particles.emit(worldState.player.position, 50, 0xffaa00, { spread: 8, lifetime: 2000 }); AudioSystem.levelUp(); }, applyToRobotEnergy() { robotEnergy.max = 100 + this.getTotalCapacityBonus(); robotEnergy.regenRate = 0.5 + this.getRegenBonus(); if (this.getCore().milestones.level50) robotEnergy.criticalThreshold = 0.15; }, XP_VALUES: { killMob: 3, killElite: 10, killBoss: 50, useAbility: 1, takeDamage: 0.5, visitPlanet: 10, discoverPOI: 5, exploreNewTile: 0.5, mineOre: 1, chopTree: 1, catchFish: 2, craftItem: 3, cookFood: 2, achievementUnlock: 25, dailyChallengeComplete: 50, surviveWave: 5 }, getLevelProgress() { const core = this.getCore(); return core.xp / this.getXPForLevel(core.level); }, getDisplayStats() { const core = this.getCore(); return { level: core.level, xp: core.xp, xpRequired: this.getXPForLevel(core.level), totalXP: core.totalXP, capacityBonus: this.getTotalCapacityBonus(), efficiencyPercent: Math.round((1.0 - this.getEfficiencyMultiplier()) * 100), regenBonus: this.getRegenBonus().toFixed(2), nextMilestone: this.getNextMilestone() }; }, getNextMilestone() { const core = this.getCore(); for (const lvl of [5, 10, 25, 50, 100]) { if (core.level < lvl) return lvl; } return null; }, init() { this.applyToRobotEnergy(); } }; // ============================================ // v12.18: PROCEDURAL INFINITE WORLD SYSTEM // Helldivers-style chunk-based procedural generation // Generates unique terrain, structures, and encounters as player explores // ============================================ const ProceduralWorldSystem = { // Chunk configuration CHUNK_SIZE: 50, // 50x50 units per chunk TILE_SIZE: 1.0, // Same as CONFIG.TILE_SIZE // v10.11: Increased load radius to prevent void when moving fast in MAKO LOAD_RADIUS: 6, // Load chunks within 6 chunk radius (13x13 = 169 chunks, 650 units) UNLOAD_RADIUS: 8, // Unload chunks beyond 8 chunk radius // State enabled: false, chunks: new Map(), // "cx,cz" -> chunk data loadedChunks: new Set(), // Currently rendered chunk keys chunkMeshes: new Map(), // "cx,cz" -> Three.js group worldSeed: 0, // Global seed for this planet biome: null, // Performance lastUpdateTime: 0, UPDATE_INTERVAL: 100, // v10.11: Check every 100ms (was 200ms) for faster response chunksLoadedThisFrame: 0, MAX_CHUNKS_PER_FRAME: 6, // v10.11: Increased from 2 to keep up with fast movement // ========================================== // HELLDIVERS-STYLE STRUCTURE TEMPLATES // Pre-designed structures that spawn in chunks // ========================================== STRUCTURE_TEMPLATES: { // Enemy Outpost - fortified position with guards enemy_outpost: { name: 'Enemy Outpost', rarity: 0.15, minDistance: 3, // Min chunks from origin size: { x: 12, z: 12 }, mobCount: { min: 3, max: 6 }, hasElite: true, loot: ['Ore', 'Ore', 'Gold Nugget'], build: function(cx, cz, rng, biome) { const structures = []; const centerX = cx * 50 + 25; const centerZ = cz * 50 + 25; // Walls for (let i = 0; i < 4; i++) { const angle = (i / 4) * Math.PI * 2; structures.push({ type: 'wall', x: centerX + Math.cos(angle) * 5, z: centerZ + Math.sin(angle) * 5, rotation: angle }); } // Central tower structures.push({ type: 'tower', x: centerX, z: centerZ, height: 8 }); // Guard positions for (let i = 0; i < 3 + Math.floor(rng.next() * 3); i++) { const angle = rng.next() * Math.PI * 2; const dist = 3 + rng.next() * 4; structures.push({ type: 'mob_spawn', x: centerX + Math.cos(angle) * dist, z: centerZ + Math.sin(angle) * dist, isElite: i === 0 && rng.next() > 0.5 }); } // Loot chest structures.push({ type: 'loot_chest', x: centerX + 2, z: centerZ, tier: 2 }); return structures; } }, // Ancient Ruins - mysterious structures with lore ancient_ruins: { name: 'Ancient Ruins', rarity: 0.08, minDistance: 4, size: { x: 20, z: 20 }, mobCount: { min: 0, max: 2 }, hasElite: false, loot: ['Ancient Relic', 'Enchant Shard'], build: function(cx, cz, rng, biome) { const structures = []; const centerX = cx * 50 + 25; const centerZ = cz * 50 + 25; // Ruined columns in circle const columnCount = 6 + Math.floor(rng.next() * 4); for (let i = 0; i < columnCount; i++) { const angle = (i / columnCount) * Math.PI * 2; const dist = 6 + rng.next() * 2; const height = 3 + rng.next() * 5; const broken = rng.next() > 0.4; structures.push({ type: 'pillar', x: centerX + Math.cos(angle) * dist, z: centerZ + Math.sin(angle) * dist, height: broken ? height * 0.5 : height, broken: broken }); } // Central altar structures.push({ type: 'altar', x: centerX, z: centerZ }); // Scattered debris for (let i = 0; i < 8; i++) { structures.push({ type: 'debris', x: centerX + (rng.next() - 0.5) * 16, z: centerZ + (rng.next() - 0.5) * 16 }); } // Lore tablet structures.push({ type: 'lore_tablet', x: centerX - 3, z: centerZ }); return structures; } }, // Crashed Ship - salvageable wreckage crashed_ship: { name: 'Crashed Vessel', rarity: 0.05, minDistance: 5, size: { x: 25, z: 15 }, mobCount: { min: 2, max: 4 }, hasElite: true, loot: ['Tech Component', 'Energy Cell', 'Rare Alloy'], build: function(cx, cz, rng, biome) { const structures = []; const centerX = cx * 50 + 25; const centerZ = cz * 50 + 25; const rotation = rng.next() * Math.PI * 2; // Main hull structures.push({ type: 'ship_hull', x: centerX, z: centerZ, rotation: rotation, size: 12 }); // Wing fragments structures.push({ type: 'ship_wing', x: centerX + Math.cos(rotation + 1.5) * 8, z: centerZ + Math.sin(rotation + 1.5) * 8, rotation: rotation + rng.next() * 0.5 }); // Debris field for (let i = 0; i < 12; i++) { structures.push({ type: 'metal_debris', x: centerX + (rng.next() - 0.5) * 20, z: centerZ + (rng.next() - 0.5) * 12 }); } // Impact crater structures.push({ type: 'crater', x: centerX + Math.cos(rotation) * 5, z: centerZ + Math.sin(rotation) * 5, radius: 4 }); // Salvage points for (let i = 0; i < 3; i++) { structures.push({ type: 'salvage_point', x: centerX + (rng.next() - 0.5) * 15, z: centerZ + (rng.next() - 0.5) * 10, tier: 2 + Math.floor(rng.next() * 2) }); } return structures; } }, // Resource Cache - guarded supply depot resource_cache: { name: 'Supply Cache', rarity: 0.20, minDistance: 1, size: { x: 8, z: 8 }, mobCount: { min: 1, max: 3 }, hasElite: false, loot: ['Ore', 'Log', 'Raw Fish', 'Gold Nugget'], build: function(cx, cz, rng, biome) { const structures = []; const centerX = cx * 50 + 25; const centerZ = cz * 50 + 25; // Storage containers for (let i = 0; i < 4; i++) { structures.push({ type: 'container', x: centerX + (i % 2) * 3 - 1.5, z: centerZ + Math.floor(i / 2) * 3 - 1.5 }); } // Guard structures.push({ type: 'mob_spawn', x: centerX + 4, z: centerZ, isElite: false }); // Main loot structures.push({ type: 'loot_chest', x: centerX, z: centerZ, tier: 1 }); return structures; } }, // Monster Den - dangerous but rewarding monster_den: { name: 'Monster Den', rarity: 0.10, minDistance: 4, size: { x: 15, z: 15 }, mobCount: { min: 5, max: 8 }, hasElite: true, loot: ['Monster Fang', 'Beast Hide', 'Rare Gem'], build: function(cx, cz, rng, biome) { const structures = []; const centerX = cx * 50 + 25; const centerZ = cz * 50 + 25; // Cave entrance structures.push({ type: 'cave_entrance', x: centerX, z: centerZ }); // Bones scattered around for (let i = 0; i < 6; i++) { structures.push({ type: 'bones', x: centerX + (rng.next() - 0.5) * 12, z: centerZ + (rng.next() - 0.5) * 12 }); } // Many monsters for (let i = 0; i < 5 + Math.floor(rng.next() * 3); i++) { const angle = rng.next() * Math.PI * 2; const dist = 2 + rng.next() * 5; structures.push({ type: 'mob_spawn', x: centerX + Math.cos(angle) * dist, z: centerZ + Math.sin(angle) * dist, isElite: i === 0, isBoss: i === 0 && rng.next() > 0.7 }); } // Treasure in back structures.push({ type: 'loot_chest', x: centerX, z: centerZ - 3, tier: 3 }); return structures; } }, // Merchant Camp - safe area with trading merchant_camp: { name: 'Traveler Camp', rarity: 0.03, minDistance: 6, size: { x: 10, z: 10 }, mobCount: { min: 0, max: 0 }, hasElite: false, loot: [], build: function(cx, cz, rng, biome) { const structures = []; const centerX = cx * 50 + 25; const centerZ = cz * 50 + 25; // Tent structures.push({ type: 'tent', x: centerX, z: centerZ }); // Campfire structures.push({ type: 'campfire', x: centerX - 3, z: centerZ + 2 }); // Trading post structures.push({ type: 'trade_post', x: centerX + 2, z: centerZ - 1 }); // Healing station structures.push({ type: 'heal_point', x: centerX - 2, z: centerZ - 2 }); return structures; } } }, // ========================================== // CORE FUNCTIONS // ========================================== // Initialize for a planet init(civ, biome) { this.enabled = true; this.worldSeed = this.hashString(civ.name + civ.id); // v10.10: Store biome name (key) for BIOMES lookup, not the full object // biome can be either a string key ('Desert') or full object ({sky:..., ground:..., name:'Desert'}) if (typeof biome === 'object' && biome !== null) { // Find the BIOMES key that matches this object this.biome = Object.keys(BIOMES).find(k => BIOMES[k] === biome) || biome.name || 'Terra'; this.biomeData = biome; // Also store the full biome data for direct access } else { this.biome = biome || 'Terra'; this.biomeData = BIOMES[this.biome] || BIOMES.Terra; } this.chunks.clear(); this.loadedChunks.clear(); this.chunkMeshes.clear(); this.chunksLoadedThisFrame = 0; console.log('[ProceduralWorld] Initialized with seed:', this.worldSeed, 'biome:', this.biome); }, // Hash string to number for seeding hashString(str) { let hash = 0; for (let i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; } return Math.abs(hash); }, // Seeded random for chunk seededRandom(cx, cz, offset = 0) { const seed = this.worldSeed + cx * 73856093 + cz * 19349663 + offset; const x = Math.sin(seed) * 10000; return x - Math.floor(x); }, // SeededRNG class for consistent generation createChunkRNG(cx, cz) { const seed = this.worldSeed + cx * 73856093 + cz * 19349663; return { seed: seed, current: seed, next() { this.current = (this.current * 1103515245 + 12345) & 0x7fffffff; return this.current / 0x7fffffff; } }; }, // Get chunk coordinates from world position getChunkCoords(worldX, worldZ) { return { cx: Math.floor(worldX / this.CHUNK_SIZE), cz: Math.floor(worldZ / this.CHUNK_SIZE) }; }, // Get chunk key from coordinates getChunkKey(cx, cz) { return cx + ',' + cz; }, // Calculate distance squared from origin in chunks - v8.08: avoid sqrt where possible getChunkDistanceSq(cx, cz) { return cx * cx + cz * cz; }, // Calculate distance from origin in chunks (only when actual distance needed) getChunkDistance(cx, cz) { return Math.sqrt(this.getChunkDistanceSq(cx, cz)); }, // ========================================== // CHUNK GENERATION // ========================================== // Generate terrain data for a chunk // v10.14: Fixed terrain heights and water to be sparse pond-like features generateChunkTerrain(cx, cz) { const rng = this.createChunkRNG(cx, cz); const terrainData = []; const startX = cx * this.CHUNK_SIZE; const startZ = cz * this.CHUNK_SIZE; for (let lx = 0; lx < this.CHUNK_SIZE; lx++) { for (let lz = 0; lz < this.CHUNK_SIZE; lz++) { const worldX = startX + lx; const worldZ = startZ + lz; // v10.16: NO water tiles in terrain - water effect is player-centered only const isWater = false; // Ground is always flat at Y=-0.5 (top surface at Y=0) const realY = -0.5; terrainData.push({ lx, lz, worldX, worldZ, height: isWater ? 0 : 1, realY, isWater }); } } return terrainData; }, // Select structure for chunk based on distance and rarity selectStructure(cx, cz) { const rng = this.createChunkRNG(cx, cz); const distance = this.getChunkDistance(cx, cz); // Origin chunk (0,0) is always safe spawn area if (cx === 0 && cz === 0) return null; // Check each structure type const candidates = []; for (const [key, template] of Object.entries(this.STRUCTURE_TEMPLATES)) { if (distance >= template.minDistance) { // Roll for this structure const roll = this.seededRandom(cx, cz, key.length); if (roll < template.rarity) { candidates.push({ key, template, roll }); } } } // Select highest priority (rarest that rolled) if (candidates.length > 0) { candidates.sort((a, b) => a.roll - b.roll); return candidates[0]; } return null; }, // Generate a complete chunk generateChunk(cx, cz) { const key = this.getChunkKey(cx, cz); if (this.chunks.has(key)) return this.chunks.get(key); const rng = this.createChunkRNG(cx, cz); const chunk = { cx, cz, key, terrain: this.generateChunkTerrain(cx, cz), structure: this.selectStructure(cx, cz), props: [], mobs: [], loaded: false, generated: true }; // Generate random props (trees, rocks) // v12.26: Increased density to 8% (was 3%) - 8-Agent Consensus const propDensity = 0.08; for (const tile of chunk.terrain) { if (!tile.isWater && rng.next() < propDensity) { chunk.props.push({ type: rng.next() > 0.5 ? 'tree' : 'rock', x: tile.worldX, z: tile.worldZ, y: tile.realY + 0.5 }); } } // Add structure elements if (chunk.structure) { const structData = chunk.structure.template.build(cx, cz, rng, this.biome); chunk.structureData = structData; } this.chunks.set(key, chunk); return chunk; }, // ========================================== // CHUNK RENDERING // ========================================== // Load chunk into scene loadChunk(cx, cz) { const key = this.getChunkKey(cx, cz); if (this.loadedChunks.has(key)) return; if (this.chunksLoadedThisFrame >= this.MAX_CHUNKS_PER_FRAME) return; const chunk = this.generateChunk(cx, cz); if (!chunk) return; const group = new THREE.Group(); group.name = 'chunk_' + key; // Create terrain mesh (instanced for performance) const groundGeo = new THREE.BoxGeometry(1, 1, 1); // v10.10: Use stored biomeData directly, fall back to BIOMES lookup const biomeData = this.biomeData || (typeof BIOMES !== 'undefined' ? (BIOMES[this.biome] || BIOMES.Terra) : { ground: 0x4a7c3f }); const groundMat = new THREE.MeshLambertMaterial({ color: biomeData.ground || 0x4a7c3f }); // v10.10: Use biome-specific water color const waterMat = new THREE.MeshLambertMaterial({ color: biomeData.water || 0x3388ff, transparent: true, opacity: 0.7 }); let groundCount = 0, waterCount = 0; for (const tile of chunk.terrain) { if (tile.isWater) waterCount++; else groundCount++; } const groundInstanced = new THREE.InstancedMesh(groundGeo, groundMat, Math.max(1, groundCount)); const waterInstanced = new THREE.InstancedMesh(groundGeo, waterMat, Math.max(1, waterCount)); let gi = 0, wi = 0; const matrix = new THREE.Matrix4(); for (const tile of chunk.terrain) { matrix.setPosition(tile.worldX, tile.realY, tile.worldZ); if (tile.isWater) { waterInstanced.setMatrixAt(wi++, matrix); } else { groundInstanced.setMatrixAt(gi++, matrix); } } groundInstanced.instanceMatrix.needsUpdate = true; waterInstanced.instanceMatrix.needsUpdate = true; group.add(groundInstanced); if (waterCount > 0) group.add(waterInstanced); // Add props (trees, rocks) for (const prop of chunk.props) { const propMesh = this.createPropMesh(prop); if (propMesh) group.add(propMesh); } // Add structure if (chunk.structureData) { for (const element of chunk.structureData) { const structMesh = this.createStructureMesh(element, chunk.structure.template); if (structMesh) group.add(structMesh); } // Show discovery notification on first load if (!chunk.discovered) { chunk.discovered = true; setTimeout(() => { showNotification('🗺️ Discovered: ' + chunk.structure.template.name, 'info'); }, 500); } } scene.add(group); this.chunkMeshes.set(key, group); this.loadedChunks.add(key); chunk.loaded = true; this.chunksLoadedThisFrame++; }, // Create prop mesh - v10.10: Rich tree variety matching main createProp system createPropMesh(prop) { if (prop.type === 'tree') { const group = new THREE.Group(); // Deterministic seed based on world position const seed = Math.abs(prop.x * 73856093 ^ prop.z * 19349663) % 1000000; const rng = { val: seed, next() { this.val = (this.val * 9301 + 49297) % 233280; return this.val / 233280; } }; // Biome-specific colors const biomeName = this.biome || 'Terra'; const TREE_COLORS = { Terra: { trunk: 0x8B4513, leaf: 0x228B22, leafAlt: 0x2E8B57 }, Desert: { trunk: 0xA0522D, leaf: 0x9ACD32, leafAlt: 0x6B8E23 }, Ice: { trunk: 0x708090, leaf: 0x87CEEB, leafAlt: 0xADD8E6 }, Volcanic: { trunk: 0x2F2F2F, leaf: 0xFF4500, leafAlt: 0xFF6347 }, Alien: { trunk: 0x8B008B, leaf: 0xFF00FF, leafAlt: 0x9400D3 }, Ocean: { trunk: 0x5F9EA0, leaf: 0x00CED1, leafAlt: 0x20B2AA }, Swamp: { trunk: 0x556B2F, leaf: 0x6B8E23, leafAlt: 0x808000 }, Crystal: { trunk: 0x4169E1, leaf: 0x00FFFF, leafAlt: 0x7FFFD4 }, Factory: { trunk: 0x555555, leaf: 0x666677, leafAlt: 0x556666 } }; const colors = TREE_COLORS[biomeName] || TREE_COLORS.Terra; const scale = 0.7 + rng.next() * 0.6; // Get trunk and leaf materials (use MinecraftTextures if available) const getTrunkMat = () => { if (typeof MinecraftTextures !== 'undefined' && MinecraftTextures.createWoodMaterial) { return MinecraftTextures.createWoodMaterial(colors.trunk, seed); } return new THREE.MeshLambertMaterial({ color: colors.trunk }); }; const getLeafMat = (color) => { if (typeof MinecraftTextures !== 'undefined' && MinecraftTextures.createLeafMaterial) { return MinecraftTextures.createLeafMaterial(color, seed + 100); } return new THREE.MeshLambertMaterial({ color: color }); }; // Tree style distribution by biome (weighted thresholds) const BIOME_TREE_DIST = { Terra: [0.25, 0.50, 0.70, 0.85, 1.0], // balanced variety Desert: [0.10, 0.25, 0.35, 0.55, 1.0], // more tall sparse trees Ice: [0.10, 0.25, 0.70, 0.85, 1.0], // heavy pine (30%+) Volcanic: [0.05, 0.20, 0.35, 0.55, 1.0], // sparse twisted Alien: [0.20, 0.45, 0.60, 0.80, 1.0], // more variety Ocean: [0.15, 0.40, 0.55, 0.80, 1.0], // kelp-like Swamp: [0.10, 0.30, 0.45, 0.75, 1.0], // more bushy mangroves Crystal: [0.15, 0.40, 0.55, 0.75, 1.0], // geometric Factory: [0.05, 0.20, 0.40, 0.70, 1.0] // sparse industrial }; const dist = BIOME_TREE_DIST[biomeName] || BIOME_TREE_DIST.Terra; const roll = rng.next(); let treeStyle = 4; // Default to tall for (let i = 0; i < dist.length; i++) { if (roll < dist[i]) { treeStyle = i; break; } } let treeName = 'Tree'; const trunkMat = getTrunkMat(); if (treeStyle === 0) { // Style 0: Multi-branch organic tree (simplified L-system look) // Main trunk const trunkGeo = new THREE.CylinderGeometry(0.12 * scale, 0.22 * scale, 2 * scale, 6); const trunk = new THREE.Mesh(trunkGeo, trunkMat); trunk.position.y = scale; trunk.castShadow = true; group.add(trunk); // Branch cluster foliage with multiple overlapping spheres const foliageMat = getLeafMat(colors.leaf); const foliageMat2 = getLeafMat(colors.leafAlt); // Main canopy const mainFoliage = new THREE.Mesh( new THREE.SphereGeometry(0.9 * scale, 8, 6), foliageMat ); mainFoliage.position.y = 2.3 * scale; mainFoliage.castShadow = true; group.add(mainFoliage); // Side branches (3-4 smaller spheres) const branchCount = 3 + Math.floor(rng.next() * 2); for (let i = 0; i < branchCount; i++) { const angle = (i / branchCount) * Math.PI * 2 + rng.next() * 0.5; const dist = 0.5 + rng.next() * 0.3; const bGeo = new THREE.SphereGeometry((0.4 + rng.next() * 0.3) * scale, 6, 5); const branch = new THREE.Mesh(bGeo, i % 2 === 0 ? foliageMat : foliageMat2); branch.position.set( Math.cos(angle) * dist * scale, (1.8 + rng.next() * 0.8) * scale, Math.sin(angle) * dist * scale ); branch.castShadow = true; group.add(branch); } treeName = 'Oak Tree'; } else if (treeStyle === 1) { // Style 1: Round sphere-top tree const trunkGeo = new THREE.CylinderGeometry(0.15 * scale, 0.25 * scale, 2 * scale, 6); const trunk = new THREE.Mesh(trunkGeo, trunkMat); trunk.position.y = scale; trunk.castShadow = true; group.add(trunk); const foliageGeo = new THREE.SphereGeometry(0.85 * scale, 8, 6); const foliage = new THREE.Mesh(foliageGeo, getLeafMat(colors.leaf)); foliage.position.y = 2.3 * scale; foliage.castShadow = true; group.add(foliage); treeName = 'Round Tree'; } else if (treeStyle === 2) { // Style 2: Pine/Cone tree with stacked layers const trunkGeo = new THREE.CylinderGeometry(0.1 * scale, 0.2 * scale, 1.8 * scale, 6); const trunk = new THREE.Mesh(trunkGeo, trunkMat); trunk.position.y = 0.9 * scale; trunk.castShadow = true; group.add(trunk); // Stacked cones for classic pine look const layers = 3 + Math.floor(rng.next() * 2); for (let i = 0; i < layers; i++) { const coneScale = 1 - i * 0.2; const coneGeo = new THREE.ConeGeometry(0.75 * scale * coneScale, 1.1 * scale * coneScale, 8); const coneMat = getLeafMat(i % 2 === 0 ? colors.leaf : colors.leafAlt); const cone = new THREE.Mesh(coneGeo, coneMat); cone.position.y = (1.6 + i * 0.6) * scale; cone.castShadow = true; group.add(cone); } treeName = 'Pine Tree'; } else if (treeStyle === 3) { // Style 3: Multi-sphere cluster tree (bushy) const trunkGeo = new THREE.CylinderGeometry(0.12 * scale, 0.2 * scale, 1.8 * scale, 5); const trunk = new THREE.Mesh(trunkGeo, trunkMat); trunk.position.y = 0.9 * scale; trunk.castShadow = true; group.add(trunk); // Multiple spheres for bushy canopy const spherePositions = [ { x: 0, y: 2.2, z: 0, r: 0.65 }, { x: 0.4, y: 1.9, z: 0.3, r: 0.45 }, { x: -0.35, y: 2.0, z: 0.4, r: 0.4 }, { x: 0.2, y: 2.45, z: -0.3, r: 0.35 }, { x: -0.4, y: 1.85, z: -0.25, r: 0.42 } ]; spherePositions.forEach((pos, idx) => { const sGeo = new THREE.SphereGeometry(pos.r * scale, 6, 5); const sMat = getLeafMat(idx % 2 === 0 ? colors.leaf : colors.leafAlt); const sphere = new THREE.Mesh(sGeo, sMat); sphere.position.set(pos.x * scale, pos.y * scale, pos.z * scale); sphere.castShadow = true; group.add(sphere); }); treeName = 'Bushy Tree'; } else { // Style 4: Tall thin tree (birch/aspen style) const trunkGeo = new THREE.CylinderGeometry(0.08 * scale, 0.14 * scale, 3.2 * scale, 6); // Use pale birch color for trunk const birchMat = (typeof MinecraftTextures !== 'undefined' && MinecraftTextures.createWoodMaterial) ? MinecraftTextures.createWoodMaterial(0xDDDDCC, seed) : new THREE.MeshLambertMaterial({ color: 0xDDDDCC }); const trunk = new THREE.Mesh(trunkGeo, birchMat); trunk.position.y = 1.6 * scale; trunk.castShadow = true; group.add(trunk); // Elongated ellipsoid foliage const foliageGeo = new THREE.SphereGeometry(0.5 * scale, 8, 6); foliageGeo.scale(1, 1.9, 1); const foliage = new THREE.Mesh(foliageGeo, getLeafMat(colors.leaf)); foliage.position.y = 3.4 * scale; foliage.castShadow = true; group.add(foliage); // Secondary smaller foliage cluster const foliage2Geo = new THREE.SphereGeometry(0.35 * scale, 6, 5); const foliage2 = new THREE.Mesh(foliage2Geo, getLeafMat(colors.leafAlt)); foliage2.position.set(0.2 * scale, 2.7 * scale, 0.1 * scale); foliage2.castShadow = true; group.add(foliage2); treeName = 'Tall Tree'; } group.position.set(prop.x, prop.y, prop.z); group.rotation.y = rng.next() * Math.PI * 2; // Random rotation group.userData = { type: 'tree', interactable: true, hp: 3, maxHp: 3, name: treeName, biomeName }; return group; } else if (prop.type === 'rock') { // v10.10: Rock variety matching main createProp const seed = Math.abs(prop.x * 73856093 ^ prop.z * 19349663) % 1000000; const rockStyle = seed % 3; const sizeVar = 0.5 + ((seed % 100) / 100) * 0.5; let rockGeo; if (rockStyle === 0) { rockGeo = new THREE.DodecahedronGeometry(0.5 + sizeVar * 0.4); } else if (rockStyle === 1) { rockGeo = new THREE.IcosahedronGeometry(0.45 + sizeVar * 0.45); } else { rockGeo = new THREE.OctahedronGeometry(0.4 + sizeVar * 0.35); } // Biome-specific rock colors const biomeName = this.biome || 'Terra'; const ROCK_COLORS = { Terra: 0x888888, Desert: 0xaa5522, Ice: 0x99aabb, Volcanic: 0x333333, Alien: 0x00ffcc, Ocean: 0x446688, Swamp: 0x445544, Crystal: 0x6688aa, Factory: 0x555566 }; const rockColor = ROCK_COLORS[biomeName] || 0x888888; const rockMat = (typeof MinecraftTextures !== 'undefined' && MinecraftTextures.createRockMaterial) ? MinecraftTextures.createRockMaterial({ rock: rockColor }) : new THREE.MeshLambertMaterial({ color: rockColor }); const rock = new THREE.Mesh(rockGeo, rockMat); rock.position.set(prop.x, prop.y, prop.z); rock.rotation.set( ((seed * 17) % 314) / 100, ((seed * 31) % 628) / 100, ((seed * 47) % 314) / 100 ); rock.scale.set(1 + sizeVar * 0.3, 0.6 + sizeVar * 0.6, 1 + sizeVar * 0.3); rock.castShadow = true; rock.userData = { type: 'rock', interactable: true }; return rock; } return null; }, // Create structure element mesh createStructureMesh(element, template) { const y = this.getTerrainHeight(element.x, element.z) + 0.5; switch (element.type) { case 'wall': const wall = new THREE.Mesh( new THREE.BoxGeometry(3, 2, 0.3), new THREE.MeshLambertMaterial({ color: 0x555555 }) ); wall.position.set(element.x, y + 1, element.z); wall.rotation.y = element.rotation || 0; return wall; case 'tower': const tower = new THREE.Mesh( new THREE.CylinderGeometry(1, 1.5, element.height || 6, 8), new THREE.MeshLambertMaterial({ color: 0x666666 }) ); tower.position.set(element.x, y + (element.height || 6) / 2, element.z); return tower; case 'pillar': const pillar = new THREE.Mesh( new THREE.CylinderGeometry(0.4, 0.5, element.height || 4, 8), new THREE.MeshLambertMaterial({ color: element.broken ? 0x999988 : 0xaaaaaa }) ); pillar.position.set(element.x, y + (element.height || 4) / 2, element.z); if (element.broken) pillar.rotation.x = 0.2; return pillar; case 'altar': const altar = new THREE.Mesh( new THREE.BoxGeometry(2, 0.5, 2), new THREE.MeshLambertMaterial({ color: 0xddddcc, emissive: 0x222211 }) ); altar.position.set(element.x, y + 0.25, element.z); return altar; case 'container': const container = new THREE.Mesh( new THREE.BoxGeometry(1.5, 1, 1), new THREE.MeshLambertMaterial({ color: 0x886633 }) ); container.position.set(element.x, y + 0.5, element.z); container.userData = { type: 'container', interactable: true }; return container; case 'loot_chest': const chest = new THREE.Mesh( new THREE.BoxGeometry(0.8, 0.6, 0.6), new THREE.MeshLambertMaterial({ color: 0xddaa44, emissive: 0x332200 }) ); chest.position.set(element.x, y + 0.3, element.z); chest.userData = { type: 'loot_chest', tier: element.tier || 1, interactable: true }; return chest; case 'mob_spawn': // Mark position for mob spawning if (typeof worldState !== 'undefined') { setTimeout(() => { if (typeof spawnMob === 'function') { const mobType = element.isBoss ? 'boss' : (element.isElite ? 'elite' : 'normal'); // Queue mob spawn this.queueMobSpawn(element.x, y, element.z, mobType); } }, 1000); } return null; case 'campfire': const fire = new THREE.Mesh( new THREE.ConeGeometry(0.3, 0.8, 6), new THREE.MeshBasicMaterial({ color: 0xff6600 }) ); fire.position.set(element.x, y + 0.4, element.z); const fireLight = new THREE.PointLight(0xff6600, 0.5, 8); fireLight.position.copy(fire.position); fire.add(fireLight); return fire; case 'tent': const tent = new THREE.Mesh( new THREE.ConeGeometry(2, 2.5, 4), new THREE.MeshLambertMaterial({ color: 0xccbb99 }) ); tent.position.set(element.x, y + 1.25, element.z); tent.rotation.y = Math.PI / 4; return tent; case 'heal_point': const healPoint = new THREE.Mesh( new THREE.SphereGeometry(0.5, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.6 }) ); healPoint.position.set(element.x, y + 1, element.z); healPoint.userData = { type: 'heal_point', interactable: true }; return healPoint; case 'debris': case 'metal_debris': case 'bones': const debris = new THREE.Mesh( new THREE.BoxGeometry(0.5 + Math.random() * 0.5, 0.2, 0.5 + Math.random() * 0.5), new THREE.MeshLambertMaterial({ color: element.type === 'bones' ? 0xeeeecc : element.type === 'metal_debris' ? 0x666677 : 0x998877 }) ); debris.position.set(element.x, y + 0.1, element.z); debris.rotation.set(Math.random() * 0.5, Math.random() * Math.PI, Math.random() * 0.5); return debris; case 'ship_hull': // v10.12: Replaced CapsuleGeometry with CylinderGeometry (wider compatibility) const hullLength = element.size || 8; const hull = new THREE.Mesh( new THREE.CylinderGeometry(2, 2, hullLength, 8), new THREE.MeshLambertMaterial({ color: 0x445566 }) ); hull.position.set(element.x, y + 2, element.z); hull.rotation.set(0.3, element.rotation || 0, 0.1); return hull; case 'crater': const crater = new THREE.Mesh( new THREE.CircleGeometry(element.radius || 3, 16), new THREE.MeshLambertMaterial({ color: 0x333322 }) ); crater.position.set(element.x, y + 0.02, element.z); crater.rotation.x = -Math.PI / 2; return crater; case 'cave_entrance': const cave = new THREE.Mesh( new THREE.TorusGeometry(2, 1, 8, 16, Math.PI), new THREE.MeshLambertMaterial({ color: 0x333333 }) ); cave.position.set(element.x, y + 1, element.z); cave.rotation.x = Math.PI / 2; return cave; default: return null; } }, // Get terrain height at position getTerrainHeight(worldX, worldZ) { const noiseScale = 0.02; const hVal = noise(worldX * noiseScale + this.worldSeed * 0.001, worldZ * noiseScale + this.worldSeed * 0.001); const height = Math.floor((hVal + 1) * 3); return Math.max(0.3, height * 0.5); }, // Queue mob spawn mobSpawnQueue: [], queueMobSpawn(x, y, z, type) { this.mobSpawnQueue.push({ x, y, z, type, time: performance.now() }); }, // Process mob spawn queue processMobSpawns() { if (this.mobSpawnQueue.length === 0) return; const now = performance.now(); const toSpawn = this.mobSpawnQueue.filter(m => now - m.time > 500); for (const mob of toSpawn) { if (typeof spawnMobAt === 'function') { spawnMobAt(mob.x, mob.y, mob.z, mob.type === 'elite', mob.type === 'boss'); } } this.mobSpawnQueue = this.mobSpawnQueue.filter(m => now - m.time <= 500); }, // Unload chunk from scene unloadChunk(cx, cz) { const key = this.getChunkKey(cx, cz); if (!this.loadedChunks.has(key)) return; const group = this.chunkMeshes.get(key); if (group) { // Dispose geometries and materials group.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(m => m.dispose()); } else { child.material.dispose(); } } }); scene.remove(group); this.chunkMeshes.delete(key); } this.loadedChunks.delete(key); const chunk = this.chunks.get(key); if (chunk) chunk.loaded = false; }, // ========================================== // UPDATE LOOP // ========================================== update() { if (!this.enabled) return; if (!worldState.player) return; const now = performance.now(); if (now - this.lastUpdateTime < this.UPDATE_INTERVAL) return; this.lastUpdateTime = now; this.chunksLoadedThisFrame = 0; // v10.11: Use MAKO position if player is in vehicle (MAKO moves faster than walking) let trackPos = worldState.player.position; if (typeof MakoVehicleSystem !== 'undefined' && MakoVehicleSystem.playerInVehicle && MakoVehicleSystem.vehicle) { trackPos = MakoVehicleSystem.vehicle.position; } const playerChunk = this.getChunkCoords(trackPos.x, trackPos.z); // v10.11: PRIORITY LOADING - Load player's chunk first (emergency bypass) // If player is in void, this chunk MUST load regardless of frame limit const playerKey = this.getChunkKey(playerChunk.cx, playerChunk.cz); if (!this.loadedChunks.has(playerKey)) { const savedLimit = this.chunksLoadedThisFrame; this.chunksLoadedThisFrame = 0; // Bypass limit for player chunk this.loadChunk(playerChunk.cx, playerChunk.cz); this.chunksLoadedThisFrame = savedLimit; } // v10.11: Load chunks in distance order (closest first) to prevent void const chunksToLoad = []; for (let dx = -this.LOAD_RADIUS; dx <= this.LOAD_RADIUS; dx++) { for (let dz = -this.LOAD_RADIUS; dz <= this.LOAD_RADIUS; dz++) { const cx = playerChunk.cx + dx; const cz = playerChunk.cz + dz; const dist = Math.abs(dx) + Math.abs(dz); // Manhattan distance chunksToLoad.push({ cx, cz, dist }); } } // Sort by distance (closest first) chunksToLoad.sort((a, b) => a.dist - b.dist); // Load chunks in priority order for (const chunk of chunksToLoad) { this.loadChunk(chunk.cx, chunk.cz); } // Unload distant chunks for (const key of this.loadedChunks) { const [cx, cz] = key.split(',').map(Number); const dx = Math.abs(cx - playerChunk.cx); const dz = Math.abs(cz - playerChunk.cz); if (dx > this.UNLOAD_RADIUS || dz > this.UNLOAD_RADIUS) { this.unloadChunk(cx, cz); } } // Process mob spawns this.processMobSpawns(); }, // Disable and cleanup disable() { this.enabled = false; // Unload all chunks for (const key of this.loadedChunks) { const [cx, cz] = key.split(',').map(Number); this.unloadChunk(cx, cz); } this.chunks.clear(); this.loadedChunks.clear(); this.chunkMeshes.clear(); }, // Get exploration stats getStats() { return { totalChunksGenerated: this.chunks.size, chunksLoaded: this.loadedChunks.size, structuresDiscovered: Array.from(this.chunks.values()).filter(c => c.structure && c.discovered).length }; } }; // v12.18: Infinite Exploration Mode Toggle // Allows player to bypass battery range limit and explore the procedurally generated world function toggleInfiniteExploration() { if (typeof gameData === 'undefined' || !gameData.settings) return; gameData.settings.infiniteExploration = !gameData.settings.infiniteExploration; const enabled = gameData.settings.infiniteExploration; if (enabled) { showNotification('🌍 INFINITE EXPLORATION ENABLED - Battery range limit removed!', 'success'); // Hide boundary visuals if (typeof BatteryRangeSystem !== 'undefined') { BatteryRangeSystem.removeBoundaryVisuals(); } } else { showNotification('🔋 Infinite exploration disabled - Battery range limit restored', 'info'); // Restore boundary visuals if (typeof BatteryRangeSystem !== 'undefined') { BatteryRangeSystem.createBoundaryVisuals(); } } // Save immediately if (typeof saveGameData === 'function') { saveGameData(); } return enabled; } window.toggleInfiniteExploration = toggleInfiniteExploration; // Add keyboard shortcut for infinite exploration (U key) document.addEventListener('keydown', (e) => { // Don't trigger when typing in inputs if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (typeof mode === 'undefined' || mode !== 'world') return; // U key - toggle infinite exploration if (e.key === 'u' || e.key === 'U') { toggleInfiniteExploration(); } }); // ============================================ // v12.19: ADAPTIVE LEARNING SYSTEM // "What you can't do today, you might be able to tomorrow" // The game AI that learns from player behavior and adapts over time // ============================================ const AdaptiveAISystem = { // Learning parameters LEARNING_RATE: 0.05, // How fast profiles update DECAY_RATE: 0.98, // How fast old data fades INSIGHT_THRESHOLD: 50, // Events needed before generating insight UPDATE_INTERVAL: 30000, // Run learning cycle every 30 seconds // Session tracking sessionStart: Date.now(), sessionEvents: [], lastUpdateTime: 0, // ========================================== // BEHAVIORAL TRACKING // ========================================== // Record a player action (called throughout the game) recordEvent(eventType, data = {}) { if (typeof gameData === 'undefined' || !gameData.adaptiveAI) return; const event = { type: eventType, timestamp: Date.now(), sessionTime: Date.now() - this.sessionStart, ...data }; this.sessionEvents.push(event); gameData.adaptiveAI.observations.totalLearningEvents++; // Immediate learning for certain events this.immediateLearn(event); // Check if it's time for a learning cycle if (Date.now() - this.lastUpdateTime > this.UPDATE_INTERVAL) { this.runLearningCycle(); } }, // Immediate learning for high-signal events immediateLearn(event) { const obs = gameData.adaptiveAI.observations; const adapt = gameData.adaptiveAI.adaptations; switch (event.type) { case 'ability_used': obs.preferredAbilities[event.ability] = (obs.preferredAbilities[event.ability] || 0) + 1; break; case 'skill_trained': obs.preferredSkills[event.skill] = (obs.preferredSkills[event.skill] || 0) + 1; // Gatherer signal if (['mining', 'wood', 'fishing'].includes(event.skill)) { this.nudgePlaystyle('gatherer', 0.01); } break; case 'mob_killed': this.nudgePlaystyle('combatant', 0.005); if (event.isBoss) this.nudgePlaystyle('combatant', 0.02); break; case 'player_death': // Track difficulty adapt.deathsBeforeLastAdjustment++; // If dying frequently, consider difficulty adjustment if (adapt.deathsBeforeLastAdjustment >= 5) { this.adjustDifficulty(-0.05); // Slightly easier } break; case 'exploration': this.nudgePlaystyle('explorer', 0.003); if (event.biome) { obs.preferredBiomes[event.biome] = (obs.preferredBiomes[event.biome] || 0) + event.duration || 1; } break; case 'structure_discovered': this.nudgePlaystyle('explorer', 0.02); this.nudgePlaystyle('completionist', 0.01); // Boost this structure type slightly adapt.structureWeights[event.structureType] = (adapt.structureWeights[event.structureType] || 1.0) * 1.02; break; case 'item_crafted': this.nudgePlaystyle('builder', 0.01); break; case 'speedrun_flag': this.nudgePlaystyle('speedrunner', 0.05); break; } }, // Nudge a playstyle value (with bounds) nudgePlaystyle(style, amount) { const playstyle = gameData.adaptiveAI.observations.playstyle; if (playstyle[style] !== undefined) { playstyle[style] = Math.max(0, Math.min(1, playstyle[style] + amount)); } }, // Adjust difficulty multiplier (with bounds) adjustDifficulty(amount) { const adapt = gameData.adaptiveAI.adaptations; adapt.difficultyMultiplier = Math.max(0.5, Math.min(2.0, adapt.difficultyMultiplier + amount)); adapt.deathsBeforeLastAdjustment = 0; adapt.killsBeforeLastAdjustment = 0; // Generate insight about adjustment const direction = amount > 0 ? 'increased' : 'decreased'; this.addInsight(`Difficulty ${direction} to ${(adapt.difficultyMultiplier * 100).toFixed(0)}% based on recent performance`, 0.8); }, // ========================================== // LEARNING CYCLES // ========================================== // Run a full learning cycle (called periodically) runLearningCycle() { if (typeof gameData === 'undefined' || !gameData.adaptiveAI) return; this.lastUpdateTime = Date.now(); gameData.adaptiveAI.learningCycles++; gameData.adaptiveAI.lastLearningUpdate = Date.now(); // Analyze session patterns this.analyzeSessionPatterns(); // Apply decay to old data this.applyDecay(); // Generate insights if enough data if (gameData.adaptiveAI.observations.totalLearningEvents >= this.INSIGHT_THRESHOLD) { this.generateInsights(); } // Log learning cycle console.log('[AdaptiveAI] Learning cycle ' + gameData.adaptiveAI.learningCycles + ' complete'); }, // Analyze session patterns analyzeSessionPatterns() { const obs = gameData.adaptiveAI.observations; // Track session length const sessionLength = (Date.now() - this.sessionStart) / 1000 / 60; // minutes if (obs.averageSessionLength === 0) { obs.averageSessionLength = sessionLength; } else { obs.averageSessionLength = obs.averageSessionLength * 0.9 + sessionLength * 0.1; } // Track peak play hours const hour = new Date().getHours(); if (!obs.peakPlayHours.includes(hour)) { obs.peakPlayHours.push(hour); if (obs.peakPlayHours.length > 5) { obs.peakPlayHours.shift(); // Keep only recent hours } } }, // Apply decay to nudge values back toward neutral over time applyDecay() { const playstyle = gameData.adaptiveAI.observations.playstyle; for (const key in playstyle) { // Decay toward 0.5 (neutral) const current = playstyle[key]; const decayedValue = 0.5 + (current - 0.5) * this.DECAY_RATE; playstyle[key] = decayedValue; } }, // Generate insights about player behavior generateInsights() { const obs = gameData.adaptiveAI.observations; const adapt = gameData.adaptiveAI.adaptations; const playstyle = obs.playstyle; // Find dominant playstyle let maxStyle = null; let maxValue = 0.55; // Threshold for "dominant" for (const [style, value] of Object.entries(playstyle)) { if (value > maxValue) { maxStyle = style; maxValue = value; } } if (maxStyle) { const insightMap = { explorer: 'You seem to love exploring! The universe is spawning more interesting structures for you.', combatant: 'Your combat prowess is noted. Expect worthy adversaries in your future.', gatherer: 'Resource collection is your forte. Rich deposits are being drawn to your path.', builder: 'A creator at heart. Crafting recipes and materials will favor your journey.', speedrunner: 'Speed demon detected. Streamlined paths are opening up.', completionist: 'Nothing escapes your notice. Hidden secrets are revealing themselves.' }; this.addInsight(insightMap[maxStyle], maxValue); } // Insight about preferred abilities const topAbility = this.getTopPreference(obs.preferredAbilities); if (topAbility) { this.addInsight(`"${topAbility}" is your signature move. Consider building around it.`, 0.7); } // Insight about preferred biomes const topBiome = this.getTopPreference(obs.preferredBiomes); if (topBiome) { this.addInsight(`${topBiome} biomes seem to resonate with your spirit.`, 0.6); } }, // Add an insight to the log addInsight(text, confidence) { const insights = gameData.adaptiveAI.insights; // Avoid duplicate insights if (insights.some(i => i.insight === text)) return; insights.push({ timestamp: Date.now(), insight: text, confidence: confidence }); // Keep only recent insights if (insights.length > 20) { insights.shift(); } // Show notification for high-confidence insights if (confidence >= 0.7 && typeof showNotification === 'function') { showNotification('🧠 ' + text, 'info'); } }, // Get top preference from a count object getTopPreference(obj) { let maxKey = null; let maxCount = 5; // Minimum threshold for (const [key, count] of Object.entries(obj)) { if (count > maxCount) { maxKey = key; maxCount = count; } } return maxKey; }, // ========================================== // ADAPTIVE RESPONSES // ========================================== // Get difficulty multiplier for mob spawning getDifficultyMultiplier() { if (typeof gameData === 'undefined' || !gameData.adaptiveAI) return 1.0; return gameData.adaptiveAI.adaptations.difficultyMultiplier; }, // Get structure weight for procedural generation getStructureWeight(structureType) { if (typeof gameData === 'undefined' || !gameData.adaptiveAI) return 1.0; return gameData.adaptiveAI.adaptations.structureWeights[structureType] || 1.0; }, // Get resource density adjustment getResourceDensity() { if (typeof gameData === 'undefined' || !gameData.adaptiveAI) return 1.0; const adapt = gameData.adaptiveAI.adaptations; const obs = gameData.adaptiveAI.observations; // Boost resources for gatherers if (obs.playstyle.gatherer > 0.6) { return adapt.resourceDensityAdjustment * 1.2; } return adapt.resourceDensityAdjustment; }, // Get mob density adjustment getMobDensity() { if (typeof gameData === 'undefined' || !gameData.adaptiveAI) return 1.0; const adapt = gameData.adaptiveAI.adaptations; const obs = gameData.adaptiveAI.observations; // Boost mobs for combatants if (obs.playstyle.combatant > 0.6) { return adapt.mobDensityAdjustment * 1.3; } return adapt.mobDensityAdjustment; }, // Check if player prefers exploration prefersExploration() { if (typeof gameData === 'undefined' || !gameData.adaptiveAI) return false; return gameData.adaptiveAI.observations.playstyle.explorer > 0.6; }, // Get current player profile summary getProfileSummary() { if (typeof gameData === 'undefined' || !gameData.adaptiveAI) { return { dominant: 'unknown', confidence: 0 }; } const playstyle = gameData.adaptiveAI.observations.playstyle; let dominant = 'balanced'; let maxValue = 0.55; for (const [style, value] of Object.entries(playstyle)) { if (value > maxValue) { dominant = style; maxValue = value; } } return { dominant, confidence: maxValue, playstyle: { ...playstyle }, learningCycles: gameData.adaptiveAI.learningCycles, insights: gameData.adaptiveAI.insights.slice(-5) }; }, // Initialize on game load init() { this.sessionStart = Date.now(); this.sessionEvents = []; this.lastUpdateTime = Date.now(); console.log('[AdaptiveAI] System initialized - Learning from player behavior'); // Show welcome message if returning player with data if (typeof gameData !== 'undefined' && gameData.adaptiveAI && gameData.adaptiveAI.learningCycles > 0) { const profile = this.getProfileSummary(); if (profile.confidence > 0.6) { setTimeout(() => { showNotification(`🧠 Welcome back, ${profile.dominant}! The game remembers you.`, 'info'); }, 3000); } } } }; // Expose globally for integration window.AdaptiveAISystem = AdaptiveAISystem; // ============================================ // v12.20: MAKO VEHICLE SYSTEM // Mass Effect 1-style planetary exploration vehicle // Extends exploration range, provides combat capabilities // Player can enter/exit to switch between scales // ============================================ const MakoVehicleSystem = { // Vehicle state active: false, deployed: false, playerInVehicle: false, // Vehicle mesh vehicleMesh: null, turretMesh: null, wheelMeshes: [], thrusterParticles: null, // Vehicle stats stats: { // Hull (vehicle HP) hull: 500, maxHull: 500, // Shields (regenerating) shields: 200, maxShields: 200, shieldRegenRate: 10, // Per second shieldRegenDelay: 3000, // ms after damage lastDamageTime: 0, // Movement maxSpeed: 25, // 5x player speed acceleration: 15, turnSpeed: 2.5, currentSpeed: 0, // Boost (Mako jump jets) boostFuel: 100, maxBoostFuel: 100, boostRegenRate: 15, boostCost: 30, boostPower: 40, boostCooldown: 0, // Weapons cannonCooldown: 0, cannonDamage: 150, cannonCooldownTime: 2000, turretCooldown: 0, turretDamage: 15, turretCooldownTime: 150, // Range extension rangeMultiplier: 8 // 8x battery range in vehicle }, // Physics velocity: new THREE.Vector3(), angularVelocity: 0, grounded: true, // Camera vehicleCameraOffset: new THREE.Vector3(0, 8, 20), vehicleCameraLookOffset: new THREE.Vector3(0, 2, 0), originalCameraSettings: null, // Interaction ENTER_DISTANCE: 15, // Increased for easier entry exitOffset: new THREE.Vector3(3, 0, 0), // v7.72: Pre-allocated temp vectors for update loop (eliminates per-frame GC) _tempForward: new THREE.Vector3(), _tempMoveVec: new THREE.Vector3(), _tempExitDir: new THREE.Vector3(), _tempOrigin: new THREE.Vector3(), // v7.91: For weapon firing // ========================================== // VEHICLE CREATION // ========================================== // Create the Mako vehicle mesh createVehicleMesh() { const group = new THREE.Group(); group.name = 'mako_vehicle'; // Main hull - armored rover body const hullGeo = new THREE.BoxGeometry(4, 1.8, 6); const hullMat = new THREE.MeshLambertMaterial({ color: 0x445566 }); const hull = new THREE.Mesh(hullGeo, hullMat); hull.position.y = 1.5; hull.castShadow = true; group.add(hull); // Angled front armor const frontGeo = new THREE.BoxGeometry(3.8, 1.2, 2); const front = new THREE.Mesh(frontGeo, hullMat); front.position.set(0, 2.2, -2.5); front.rotation.x = -0.3; group.add(front); // Cockpit (glass dome) const cockpitGeo = new THREE.SphereGeometry(1.2, 16, 12, 0, Math.PI * 2, 0, Math.PI / 2); const cockpitMat = new THREE.MeshLambertMaterial({ color: 0x88ccff, transparent: true, opacity: 0.6 }); const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat); cockpit.position.set(0, 2.4, -1); cockpit.rotation.x = -0.2; group.add(cockpit); // Turret base const turretBaseGeo = new THREE.CylinderGeometry(0.8, 1, 0.6, 8); const turretMat = new THREE.MeshLambertMaterial({ color: 0x556677 }); const turretBase = new THREE.Mesh(turretBaseGeo, turretMat); turretBase.position.set(0, 2.8, 1); group.add(turretBase); // Turret (rotatable) const turretGroup = new THREE.Group(); turretGroup.position.set(0, 3.1, 1); const turretHeadGeo = new THREE.BoxGeometry(1.2, 0.6, 1.5); const turretHead = new THREE.Mesh(turretHeadGeo, turretMat); turretGroup.add(turretHead); // Main cannon const cannonGeo = new THREE.CylinderGeometry(0.15, 0.2, 2.5, 8); const cannonMat = new THREE.MeshLambertMaterial({ color: 0x333344 }); const cannon = new THREE.Mesh(cannonGeo, cannonMat); cannon.rotation.x = Math.PI / 2; cannon.position.z = -1.8; turretGroup.add(cannon); // Machine gun const mgGeo = new THREE.CylinderGeometry(0.06, 0.08, 1.5, 6); const mg = new THREE.Mesh(mgGeo, cannonMat); mg.rotation.x = Math.PI / 2; mg.position.set(0.4, -0.15, -1.5); turretGroup.add(mg); group.add(turretGroup); this.turretMesh = turretGroup; // Wheels (6 wheels like Mako) this.wheelMeshes = []; const wheelGeo = new THREE.CylinderGeometry(0.7, 0.7, 0.5, 12); const wheelMat = new THREE.MeshLambertMaterial({ color: 0x222222 }); const wheelPositions = [ [-2, 0.7, -2], // Front left [2, 0.7, -2], // Front right [-2.2, 0.7, 0], // Mid left [2.2, 0.7, 0], // Mid right [-2, 0.7, 2], // Rear left [2, 0.7, 2] // Rear right ]; wheelPositions.forEach(pos => { const wheel = new THREE.Mesh(wheelGeo, wheelMat); wheel.position.set(pos[0], pos[1], pos[2]); wheel.rotation.z = Math.PI / 2; wheel.castShadow = true; group.add(wheel); this.wheelMeshes.push(wheel); }); // Thruster exhausts (rear) const exhaustGeo = new THREE.CylinderGeometry(0.3, 0.4, 0.8, 8); const exhaustMat = new THREE.MeshLambertMaterial({ color: 0x333333 }); [-1, 1].forEach(x => { const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat); exhaust.position.set(x, 1.2, 3.2); exhaust.rotation.x = Math.PI / 2; group.add(exhaust); }); // Shield generator glow const shieldGlowGeo = new THREE.SphereGeometry(4, 16, 16); const shieldGlowMat = new THREE.MeshBasicMaterial({ color: 0x4488ff, transparent: true, opacity: 0, side: THREE.BackSide }); const shieldGlow = new THREE.Mesh(shieldGlowGeo, shieldGlowMat); shieldGlow.name = 'shield_glow'; group.add(shieldGlow); // Headlights const lightGeo = new THREE.CircleGeometry(0.3, 8); const lightMat = new THREE.MeshBasicMaterial({ color: 0xffffcc }); [-1.2, 1.2].forEach(x => { const light = new THREE.Mesh(lightGeo, lightMat); light.position.set(x, 1.5, -3.1); group.add(light); }); // Point lights for headlights const headlight = new THREE.SpotLight(0xffffcc, 1, 50, Math.PI / 6); headlight.position.set(0, 2, -3); headlight.target.position.set(0, 0, -20); group.add(headlight); group.add(headlight.target); // Store reference this.vehicleMesh = group; // UserData for interaction group.userData = { type: 'vehicle', isVehicle: true, name: 'M-35 MAKO', interactable: true }; return group; }, // ========================================== // VEHICLE HUD // ========================================== createVehicleHUD() { // Check if HUD already exists if (document.getElementById('mako-hud')) return; const hud = document.createElement('div'); hud.id = 'mako-hud'; hud.innerHTML = `
M-35 MAKO
HULL500/500
SHIELDS200/200
BOOST100%
SPEED
0 m/s
CANNON TURRET
[V] Exit | [SPACE] Boost | [LMB] Cannon | [RMB] Turret
RANGE
FROM SHIP0m
MAX RANGE0m
RANGE BONUS8x
`; document.body.appendChild(hud); }, // v7.96: Cache HUD DOM elements to eliminate 11 getElementById calls per update _hudCache: null, getHudCache() { if (!this._hudCache) { this._hudCache = { hull: document.getElementById('mako-hull-bar'), hullText: document.getElementById('mako-hull-text'), shield: document.getElementById('mako-shield-bar'), shieldText: document.getElementById('mako-shield-text'), boost: document.getElementById('mako-boost-bar'), boostText: document.getElementById('mako-boost-text'), speed: document.getElementById('mako-speed-value'), cannon: document.getElementById('mako-cannon-status'), turret: document.getElementById('mako-turret-status'), range: document.getElementById('mako-range-text'), maxRange: document.getElementById('mako-max-range-text') }; } return this._hudCache; }, updateHUD() { if (!this.playerInVehicle) return; // v7.96: Use cached DOM references const cache = this.getHudCache(); if (cache.hull) { cache.hull.style.width = (this.stats.hull / this.stats.maxHull * 100) + '%'; cache.hullText.textContent = `${Math.floor(this.stats.hull)}/${this.stats.maxHull}`; } if (cache.shield) { cache.shield.style.width = (this.stats.shields / this.stats.maxShields * 100) + '%'; cache.shieldText.textContent = `${Math.floor(this.stats.shields)}/${this.stats.maxShields}`; } if (cache.boost) { cache.boost.style.width = (this.stats.boostFuel / this.stats.maxBoostFuel * 100) + '%'; cache.boostText.textContent = Math.floor(this.stats.boostFuel) + '%'; } if (cache.speed) { cache.speed.textContent = Math.floor(Math.abs(this.stats.currentSpeed)); } if (cache.cannon) { cache.cannon.className = this.stats.cannonCooldown <= 0 ? 'mako-weapon-ready' : 'mako-weapon-cooldown'; } if (cache.turret) { cache.turret.className = this.stats.turretCooldown <= 0 ? 'mako-weapon-ready' : 'mako-weapon-cooldown'; } if (cache.range && typeof BatteryRangeSystem !== 'undefined') { const dist = BatteryRangeSystem.getDistanceFromOrigin(); cache.range.textContent = Math.floor(dist) + 'm'; const max = BatteryRangeSystem.getMaxRange() * this.stats.rangeMultiplier; cache.maxRange.textContent = Math.floor(max) + 'm'; } }, showHUD() { const hud = document.getElementById('mako-hud'); if (hud) hud.classList.add('active'); }, hideHUD() { const hud = document.getElementById('mako-hud'); if (hud) hud.classList.remove('active'); }, // ========================================== // DEPLOYMENT & INTERACTION // ========================================== // Deploy vehicle near ship on planet landing deployVehicle(spawnX, spawnZ) { if (this.deployed) return; if (typeof scene === 'undefined') return; // Create vehicle if not exists if (!this.vehicleMesh) { this.createVehicleMesh(); } // Position near ship (offset to the side) const vehicleX = spawnX + 8; const vehicleZ = spawnZ + 5; const vehicleY = typeof getTerrainHeight === 'function' ? getTerrainHeight(vehicleX, vehicleZ) + 0.5 : 0.5; this.vehicleMesh.position.set(vehicleX, vehicleY, vehicleZ); this.vehicleMesh.rotation.y = Math.PI / 4; // Angled for cool look // Add to scene scene.add(this.vehicleMesh); // Reset stats this.stats.hull = this.stats.maxHull; this.stats.shields = this.stats.maxShields; this.stats.boostFuel = this.stats.maxBoostFuel; this.stats.currentSpeed = 0; this.velocity.set(0, 0, 0); this.deployed = true; this.playerInVehicle = false; // Create HUD this.createVehicleHUD(); console.log('[MAKO] Vehicle deployed at', vehicleX.toFixed(1), vehicleZ.toFixed(1)); showNotification('🚗 M-35 MAKO deployed near ship - Press V to enter', 'info'); }, // Remove vehicle when leaving planet recallVehicle() { if (!this.deployed) return; // Exit player first if (this.playerInVehicle) { this.exitVehicle(); } // Remove from scene if (this.vehicleMesh && typeof scene !== 'undefined') { scene.remove(this.vehicleMesh); } this.deployed = false; this.hideHUD(); console.log('[MAKO] Vehicle recalled'); }, // Check if player is near vehicle // v7.80: distanceToSquared optimization isPlayerNearVehicle() { if (!this.deployed || !this.vehicleMesh || !worldState.player) return false; const distSq = this.vehicleMesh.position.distanceToSquared(worldState.player.position); return distSq <= this.ENTER_DISTANCE * this.ENTER_DISTANCE; }, // Enter the vehicle enterVehicle() { if (!this.deployed || this.playerInVehicle) return false; if (!this.isPlayerNearVehicle()) { showNotification('Get closer to the MAKO to enter', 'warning'); return false; } this.playerInVehicle = true; // Hide player mesh if (worldState.player) { worldState.player.visible = false; } // Store original camera settings if (typeof camera !== 'undefined') { this.originalCameraSettings = { fov: camera.fov }; // Wider FOV for vehicle camera.fov = 75; camera.updateProjectionMatrix(); } // Show HUD this.showHUD(); // Sound effect if (typeof AudioSystem !== 'undefined') { AudioSystem.sfx('powerup'); } showNotification('🚗 Entered M-35 MAKO - 8x exploration range!', 'success'); console.log('[MAKO] Player entered vehicle'); // Track for adaptive AI if (typeof AdaptiveAISystem !== 'undefined') { AdaptiveAISystem.recordEvent('vehicle_entered', { vehicle: 'mako' }); } return true; }, // Exit the vehicle exitVehicle() { if (!this.playerInVehicle) return false; this.playerInVehicle = false; this.stats.currentSpeed = 0; // Show player mesh and position them next to vehicle if (worldState.player && this.vehicleMesh) { // Exit to the side of the vehicle const exitDir = new THREE.Vector3(1, 0, 0); exitDir.applyQuaternion(this.vehicleMesh.quaternion); const exitPos = this.vehicleMesh.position.clone().add(exitDir.multiplyScalar(4)); exitPos.y = typeof getTerrainHeight === 'function' ? getTerrainHeight(exitPos.x, exitPos.z) + 0.5 : 0.5; worldState.player.position.copy(exitPos); worldState.player.rotation.y = this.vehicleMesh.rotation.y; worldState.player.visible = true; } // Restore camera if (typeof camera !== 'undefined' && this.originalCameraSettings) { camera.fov = this.originalCameraSettings.fov || 60; camera.updateProjectionMatrix(); } // Hide HUD this.hideHUD(); showNotification('Exited MAKO', 'info'); console.log('[MAKO] Player exited vehicle'); return true; }, // Toggle enter/exit toggleVehicle() { if (this.playerInVehicle) { return this.exitVehicle(); } else { return this.enterVehicle(); } }, // ========================================== // VEHICLE PHYSICS & MOVEMENT // ========================================== update(dt, time, keys, mouseButtons) { if (!this.deployed || !this.vehicleMesh) return; // Regenerate shields this.updateShields(dt); // Regenerate boost this.updateBoost(dt); // Update cooldowns if (this.stats.cannonCooldown > 0) this.stats.cannonCooldown -= dt * 1000; if (this.stats.turretCooldown > 0) this.stats.turretCooldown -= dt * 1000; if (this.stats.boostCooldown > 0) this.stats.boostCooldown -= dt * 1000; // Only process movement if player is in vehicle if (this.playerInVehicle) { this.updateMovement(dt, keys); this.updateCamera(); this.updateWeapons(mouseButtons); this.updateHUD(); } // Animate wheels based on speed this.animateWheels(dt); // Update shield glow this.updateShieldVisual(); }, updateMovement(dt, keys) { const s = this.stats; // Acceleration/deceleration if (keys.w) { s.currentSpeed = Math.min(s.maxSpeed, s.currentSpeed + s.acceleration * dt); } else if (keys.s) { s.currentSpeed = Math.max(-s.maxSpeed * 0.4, s.currentSpeed - s.acceleration * dt); } else { // Friction/deceleration s.currentSpeed *= (1 - dt * 2); if (Math.abs(s.currentSpeed) < 0.1) s.currentSpeed = 0; } // Turning (only when moving) if (Math.abs(s.currentSpeed) > 0.5) { const turnFactor = Math.min(1, Math.abs(s.currentSpeed) / 10); if (keys.a) { this.vehicleMesh.rotation.y += s.turnSpeed * turnFactor * dt; } if (keys.d) { this.vehicleMesh.rotation.y -= s.turnSpeed * turnFactor * dt; } } // Calculate movement direction // v7.72: Use pre-allocated temp vectors const forward = this._tempForward.set(0, 0, -1); forward.applyQuaternion(this.vehicleMesh.quaternion); // Apply movement const moveVec = this._tempMoveVec.copy(forward).multiplyScalar(s.currentSpeed * dt); // Check battery range const newX = this.vehicleMesh.position.x + moveVec.x; const newZ = this.vehicleMesh.position.z + moveVec.z; // Vehicle has extended range // v7.77: Use squared distance comparison to avoid sqrt let canMove = true; if (typeof BatteryRangeSystem !== 'undefined' && robotEnergy.origin) { const dx = newX - robotEnergy.origin.x; const dz = newZ - robotEnergy.origin.z; const distSq = dx * dx + dz * dz; const maxRange = BatteryRangeSystem.getMaxRange() * s.rangeMultiplier; const maxRangeSq = maxRange * maxRange; // Check infinite exploration mode if (typeof gameData !== 'undefined' && gameData.settings && gameData.settings.infiniteExploration) { canMove = true; } else if (distSq > maxRangeSq) { canMove = false; showNotification('⚠️ MAKO at maximum range - return to ship!', 'warning'); } } if (canMove) { this.vehicleMesh.position.x = newX; this.vehicleMesh.position.z = newZ; // Snap to terrain if (typeof getTerrainHeight === 'function') { const groundY = getTerrainHeight(this.vehicleMesh.position.x, this.vehicleMesh.position.z); this.vehicleMesh.position.y = groundY + 0.5; } } // Update procedural world based on vehicle position if (typeof ProceduralWorldSystem !== 'undefined' && ProceduralWorldSystem.enabled) { // Temporarily set player position to vehicle for chunk loading const oldPlayerPos = worldState.player ? worldState.player.position.clone() : null; if (worldState.player) { worldState.player.position.copy(this.vehicleMesh.position); } } }, // Boost (Mako jump jets) activateBoost() { if (!this.playerInVehicle) return false; if (this.stats.boostFuel < this.stats.boostCost) { showNotification('Boost fuel depleted!', 'warning'); return false; } if (this.stats.boostCooldown > 0) return false; // Consume fuel this.stats.boostFuel -= this.stats.boostCost; this.stats.boostCooldown = 500; // Apply boost force const forward = new THREE.Vector3(0, 0, -1); forward.applyQuaternion(this.vehicleMesh.quaternion); this.stats.currentSpeed += this.stats.boostPower; // Visual effect if (typeof particles !== 'undefined') { particles.emit(this.vehicleMesh.position, 30, 0x44ff88, { spread: 4, lifetime: 500 }); } // Sound if (typeof AudioSystem !== 'undefined') { AudioSystem.sfx('dash'); } showNotification('BOOST!', 'info'); return true; }, updateBoost(dt) { // Regenerate boost fuel if (this.stats.boostFuel < this.stats.maxBoostFuel) { this.stats.boostFuel = Math.min( this.stats.maxBoostFuel, this.stats.boostFuel + this.stats.boostRegenRate * dt ); } }, updateShields(dt) { const now = performance.now(); // Regenerate shields after delay if (now - this.stats.lastDamageTime > this.stats.shieldRegenDelay) { if (this.stats.shields < this.stats.maxShields) { this.stats.shields = Math.min( this.stats.maxShields, this.stats.shields + this.stats.shieldRegenRate * dt ); } } }, updateShieldVisual() { if (!this.vehicleMesh) return; const shieldGlow = this.vehicleMesh.getObjectByName('shield_glow'); if (shieldGlow) { // Shield glow intensity based on shield level const shieldPercent = this.stats.shields / this.stats.maxShields; shieldGlow.material.opacity = shieldPercent > 0 ? 0.1 + shieldPercent * 0.1 : 0; // Flash when damaged if (performance.now() - this.stats.lastDamageTime < 200) { shieldGlow.material.opacity = 0.5; shieldGlow.material.color.setHex(0xff4444); } else { shieldGlow.material.color.setHex(0x4488ff); } } }, animateWheels(dt) { if (!this.wheelMeshes.length) return; const rotationSpeed = this.stats.currentSpeed * 0.5; this.wheelMeshes.forEach(wheel => { wheel.rotation.x += rotationSpeed * dt; }); }, // v7.91: Pre-allocated vectors for camera update (avoids 3 allocations per frame) _tempCamOffset: null, _tempCamTarget: null, _tempCamLook: null, updateCamera() { if (!this.playerInVehicle || !this.vehicleMesh || typeof camera === 'undefined') return; // v7.91: Lazy-init temp vectors if (!this._tempCamOffset) this._tempCamOffset = new THREE.Vector3(); if (!this._tempCamTarget) this._tempCamTarget = new THREE.Vector3(); if (!this._tempCamLook) this._tempCamLook = new THREE.Vector3(); // Third-person chase camera - v7.91: Use pre-allocated vectors this._tempCamOffset.copy(this.vehicleCameraOffset); this._tempCamOffset.applyQuaternion(this.vehicleMesh.quaternion); this._tempCamTarget.copy(this.vehicleMesh.position).add(this._tempCamOffset); // Smooth camera follow camera.position.lerp(this._tempCamTarget, 0.1); // Look at vehicle this._tempCamLook.copy(this.vehicleMesh.position).add(this.vehicleCameraLookOffset); camera.lookAt(this._tempCamLook); }, // ========================================== // VEHICLE COMBAT // ========================================== updateWeapons(mouseButtons) { if (!mouseButtons) return; // Left click - Main cannon if (mouseButtons.left && this.stats.cannonCooldown <= 0) { this.fireCannon(); } // Right click - Machine gun turret if (mouseButtons.right && this.stats.turretCooldown <= 0) { this.fireTurret(); } }, fireCannon() { if (this.stats.cannonCooldown > 0) return; this.stats.cannonCooldown = this.stats.cannonCooldownTime; // Get forward direction from turret - v7.91: Use pre-allocated vectors this._tempForward.set(0, 0, -1); if (this.turretMesh) { this._tempForward.applyQuaternion(this.vehicleMesh.quaternion); } this._tempOrigin.copy(this.vehicleMesh.position); this._tempOrigin.y += 3; this._tempOrigin.x += this._tempForward.x * 3; this._tempOrigin.z += this._tempForward.z * 3; // Raycast for hit detection this.fireProjectile(this._tempOrigin, this._tempForward, this.stats.cannonDamage, 'cannon'); // Visual effect if (typeof particles !== 'undefined') { particles.emit(this._tempOrigin, 20, 0xffaa00, { spread: 2, lifetime: 300 }); } // Sound if (typeof AudioSystem !== 'undefined') { AudioSystem.sfx('hit'); } // Screen shake if (typeof screenShake === 'function') { screenShake(0.3); } }, fireTurret() { if (this.stats.turretCooldown > 0) return; this.stats.turretCooldown = this.stats.turretCooldownTime; // v7.91: Use pre-allocated vectors this._tempForward.set(0, 0, -1); this._tempForward.applyQuaternion(this.vehicleMesh.quaternion); this._tempOrigin.copy(this.vehicleMesh.position); this._tempOrigin.y += 3; this._tempOrigin.x += this._tempForward.x * 2; this._tempOrigin.z += this._tempForward.z * 2; // Slight spread this._tempForward.x += (Math.random() - 0.5) * 0.1; this._tempForward.z += (Math.random() - 0.5) * 0.1; this._tempForward.normalize(); this.fireProjectile(this._tempOrigin, this._tempForward, this.stats.turretDamage, 'turret'); // Visual effect if (typeof particles !== 'undefined') { particles.emit(this._tempOrigin, 5, 0xffff00, { spread: 1, lifetime: 100 }); } }, fireProjectile(origin, direction, damage, type) { // Raycast to find hit if (typeof THREE === 'undefined') return; const raycaster = new THREE.Raycaster(origin, direction, 0, 100); const targets = []; // Add mobs as targets if (typeof worldState !== 'undefined' && worldState.mobs) { targets.push(...worldState.mobs.filter(m => m && m.userData && m.userData.hp > 0)); } const hits = raycaster.intersectObjects(targets, true); if (hits.length > 0) { const hit = hits[0]; let targetMob = hit.object; // Find parent mob while (targetMob.parent && !targetMob.userData?.hp) { targetMob = targetMob.parent; } if (targetMob.userData && targetMob.userData.hp > 0) { // Apply damage targetMob.userData.hp -= damage; // Damage number if (typeof spawnFloater === 'function') { spawnFloater(hit.point, `-${damage}`, type === 'cannon' ? '#ff6600' : '#ffff00'); } // Impact particles if (typeof particles !== 'undefined') { particles.emit(hit.point, 15, 0xff4400, { spread: 3, lifetime: 400 }); } // Check kill if (targetMob.userData.hp <= 0) { showNotification(`Target destroyed!`, 'success'); } } } }, // ========================================== // DAMAGE HANDLING // ========================================== takeDamage(amount, damageType = 'normal') { if (!this.playerInVehicle) return; this.stats.lastDamageTime = performance.now(); // Shields absorb damage first if (this.stats.shields > 0) { const shieldDamage = Math.min(this.stats.shields, amount); this.stats.shields -= shieldDamage; amount -= shieldDamage; if (this.stats.shields <= 0) { showNotification('⚠️ SHIELDS DOWN!', 'warning'); } } // Remaining damage goes to hull if (amount > 0) { this.stats.hull -= amount; // Screen shake if (typeof screenShake === 'function') { screenShake(0.2 + amount / 100); } // Critical damage warning if (this.stats.hull <= this.stats.maxHull * 0.25) { showNotification('⚠️ HULL CRITICAL!', 'error'); } // Vehicle destroyed if (this.stats.hull <= 0) { this.destroyVehicle(); } } }, destroyVehicle() { showNotification('💥 MAKO DESTROYED!', 'error'); // Force exit this.exitVehicle(); // Explosion effect if (typeof particles !== 'undefined' && this.vehicleMesh) { particles.emit(this.vehicleMesh.position, 100, 0xff4400, { spread: 10, lifetime: 2000 }); } // Remove vehicle if (this.vehicleMesh && typeof scene !== 'undefined') { scene.remove(this.vehicleMesh); } this.deployed = false; // Damage player if (typeof damagePlayer === 'function') { damagePlayer(50, 'vehicle_explosion'); } }, // ========================================== // RANGE OVERRIDE // ========================================== // Get effective battery range (8x when in vehicle) getEffectiveRangeMultiplier() { return this.playerInVehicle ? this.stats.rangeMultiplier : 1.0; }, // Check if position is in vehicle range - v8.08: uses squared distance isInVehicleRange(x, z) { if (!this.playerInVehicle) return true; // Not in vehicle, use normal check if (typeof BatteryRangeSystem === 'undefined' || !robotEnergy.origin) return true; const dx = x - robotEnergy.origin.x; const dz = z - robotEnergy.origin.z; const distSq = dx * dx + dz * dz; const maxRange = BatteryRangeSystem.getMaxRange() * this.stats.rangeMultiplier; return distSq <= maxRange * maxRange; } }; // Expose globally window.MakoVehicleSystem = MakoVehicleSystem; // Add keyboard shortcut for vehicle (V key) document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if (typeof mode === 'undefined' || mode !== 'world') return; // V key - toggle vehicle entry/exit if (e.key === 'v' || e.key === 'V') { if (typeof MakoVehicleSystem !== 'undefined') { if (MakoVehicleSystem.deployed) { MakoVehicleSystem.toggleVehicle(); } else { showNotification('No vehicle deployed - land on a planet first', 'warning'); } } } // Space - boost when in vehicle if (e.key === ' ' && MakoVehicleSystem.playerInVehicle) { e.preventDefault(); MakoVehicleSystem.activateBoost(); } }); // ============================================ // v9.8: CINEMATIC LANDING SEQUENCE // Breath of the Wild-style intro cutscene // Spacecraft descent → Landing → Robot emerges → Environment reveal // ============================================ const LandingSequence = { // State active: false, phase: 'idle', // idle, fadeIn, descent, landing, doorOpen, emerge, cameraReveal, fadeOut, complete time: 0, phaseTime: 0, skipRequested: false, // Scene elements spacecraft: null, ramp: null, thrusterParticles: [], dustParticles: [], _tempParticleVelocity: new THREE.Vector3(), // v7.84: Pre-allocated for particle updates // Camera cinematicCamera: null, originalCameraPos: null, originalCameraTarget: null, // Landing position landingPos: new THREE.Vector3(), landingY: 0, // Phase durations (seconds) phaseDurations: { fadeIn: 1.5, descent: 4.0, landing: 1.5, doorOpen: 2.0, emerge: 3.5, cameraReveal: 5.0, fadeOut: 1.5 }, // UI Elements overlay: null, skipHint: null, titleCard: null, // Create spacecraft with animated ramp createSpacecraft() { const shipGroup = new THREE.Group(); // Main body - sleek fuselage const bodyGeometry = new THREE.BoxGeometry(4, 1.5, 5); const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0x2a2a3a, metalness: 0.7, roughness: 0.3 }); const body = new THREE.Mesh(bodyGeometry, bodyMaterial); body.castShadow = true; body.receiveShadow = true; shipGroup.add(body); // Cockpit dome with glow const cockpitGeometry = new THREE.SphereGeometry(1.2, 16, 16); const cockpitMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, metalness: 0.9, roughness: 0.1, emissive: 0x004444, emissiveIntensity: 0.5 }); const cockpit = new THREE.Mesh(cockpitGeometry, cockpitMaterial); cockpit.position.set(0, 0.8, 0.5); cockpit.scale.set(1, 0.5, 1.2); shipGroup.add(cockpit); shipGroup.userData.cockpit = cockpit; // Wings const wingGeometry = new THREE.BoxGeometry(8, 0.2, 2); const wingMaterial = new THREE.MeshStandardMaterial({ color: 0x3a3a4a, metalness: 0.6, roughness: 0.4 }); const wings = new THREE.Mesh(wingGeometry, wingMaterial); wings.position.set(0, 0.3, -0.5); wings.castShadow = true; shipGroup.add(wings); // Wing tip lights const tipGeometry = new THREE.BoxGeometry(0.5, 0.3, 0.5); const tipMaterialL = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 1 }); const tipMaterialR = new THREE.MeshStandardMaterial({ color: 0x00ff00, emissive: 0x00ff00, emissiveIntensity: 1 }); const tipL = new THREE.Mesh(tipGeometry, tipMaterialL); const tipR = new THREE.Mesh(tipGeometry, tipMaterialR); tipL.position.set(-4, 0.3, -0.5); tipR.position.set(4, 0.3, -0.5); shipGroup.add(tipL, tipR); shipGroup.userData.tipL = tipL; shipGroup.userData.tipR = tipR; // Tail fin const tailGeometry = new THREE.BoxGeometry(0.3, 2, 1); const tail = new THREE.Mesh(tailGeometry, wingMaterial); tail.position.set(0, 1, -2); tail.castShadow = true; shipGroup.add(tail); // Engine pods with animated glow const engineGeometry = new THREE.CylinderGeometry(0.4, 0.5, 2, 8); const engineMaterial = new THREE.MeshStandardMaterial({ color: 0x1a1a2a, metalness: 0.8, roughness: 0.2 }); shipGroup.userData.engineGlows = []; [-2, 2].forEach(x => { const engine = new THREE.Mesh(engineGeometry, engineMaterial); engine.rotation.x = Math.PI / 2; engine.position.set(x, 0, -2); engine.castShadow = true; shipGroup.add(engine); // Engine glow (animated) const glowGeometry = new THREE.CircleGeometry(0.5, 16); const glowMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.9 }); const glow = new THREE.Mesh(glowGeometry, glowMaterial); glow.rotation.x = -Math.PI / 2; glow.position.set(x, -0.1, -3); shipGroup.add(glow); shipGroup.userData.engineGlows.push(glow); }); // Landing gear (retractable) const gearGeometry = new THREE.BoxGeometry(0.3, 1.2, 0.3); const gearMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.5 }); shipGroup.userData.landingGear = []; [[-1.5, -0.5, 1], [1.5, -0.5, 1], [0, -0.5, -2]].forEach(pos => { const gear = new THREE.Mesh(gearGeometry, gearMaterial); gear.position.set(...pos); gear.castShadow = true; gear.userData.restY = pos[1]; gear.userData.deployedY = pos[1] - 0.8; shipGroup.add(gear); shipGroup.userData.landingGear.push(gear); }); // Exit ramp (animates down) const rampGroup = new THREE.Group(); rampGroup.position.set(0, -0.75, 2.5); const rampGeometry = new THREE.BoxGeometry(1.5, 0.15, 2.5); const rampMaterial = new THREE.MeshStandardMaterial({ color: 0x3a3a4a, metalness: 0.5, roughness: 0.5 }); const ramp = new THREE.Mesh(rampGeometry, rampMaterial); ramp.position.set(0, 0, 1.25); ramp.castShadow = true; rampGroup.add(ramp); // Ramp lights const rampLightGeo = new THREE.BoxGeometry(0.1, 0.05, 2.3); const rampLightMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, transparent: true, opacity: 0.8 }); [-0.65, 0.65].forEach(x => { const light = new THREE.Mesh(rampLightGeo, rampLightMat); light.position.set(x, 0.1, 1.15); rampGroup.add(light); }); rampGroup.rotation.x = Math.PI / 2; // Start closed (vertical) shipGroup.add(rampGroup); shipGroup.userData.ramp = rampGroup; // Interior light (visible when door opens) const interiorLight = new THREE.PointLight(0x00ffff, 2, 8); interiorLight.position.set(0, 0.5, 1.5); interiorLight.visible = false; shipGroup.add(interiorLight); shipGroup.userData.interiorLight = interiorLight; // Spotlight for dramatic landing const spotlight = new THREE.SpotLight(0xffffff, 3, 50, Math.PI / 6, 0.5); spotlight.position.set(0, -1, 0); spotlight.target.position.set(0, -10, 0); shipGroup.add(spotlight); shipGroup.add(spotlight.target); shipGroup.userData.spotlight = spotlight; return shipGroup; }, // Create thruster particle createThrusterParticle(position, velocity) { const geo = new THREE.SphereGeometry(0.15 + Math.random() * 0.1, 8, 8); const mat = new THREE.MeshBasicMaterial({ color: new THREE.Color().setHSL(0.35 + Math.random() * 0.1, 1, 0.6), transparent: true, opacity: 0.9 }); const particle = new THREE.Mesh(geo, mat); particle.position.copy(position); particle.userData.velocity = velocity.clone(); particle.userData.life = 1.0; particle.userData.decay = 1.5 + Math.random() * 0.5; return particle; }, // Create dust particle createDustParticle(position) { const geo = new THREE.SphereGeometry(0.3 + Math.random() * 0.4, 8, 8); const mat = new THREE.MeshBasicMaterial({ color: new THREE.Color(0.6, 0.5, 0.4), transparent: true, opacity: 0.6 }); const particle = new THREE.Mesh(geo, mat); particle.position.copy(position); const angle = Math.random() * Math.PI * 2; const speed = 3 + Math.random() * 5; particle.userData.velocity = new THREE.Vector3( Math.cos(angle) * speed, 1 + Math.random() * 2, Math.sin(angle) * speed ); particle.userData.life = 1.0; particle.userData.decay = 0.4 + Math.random() * 0.3; return particle; }, // Ambient music state ambientMusic: null, // Play gentle ambient music/sounds - no harsh noises playSound(type) { if (!AudioSystem || !AudioSystem.ctx) return; const ctx = AudioSystem.ctx; const now = ctx.currentTime; if (type === 'engine') { // Start gentle ambient music that plays throughout the sequence // Beautiful, dreamy pad with slow attack - like waking up to a new world this.startAmbientMusic(); } else if (type === 'landing') { // Soft, gentle "arrival" chime - like a bell in the distance this.playChime([523.25, 659.25, 783.99], 0.06, 2.5); // C5, E5, G5 major chord } else if (type === 'hydraulic') { // Gentle ascending shimmer - sense of opening/possibility this.playShimmer(400, 800, 1.5, 0.04); } else if (type === 'footstep') { // Very soft, subtle footstep - barely audible // Skip most footsteps for a more peaceful experience if (Math.random() > 0.7) { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = 200 + Math.random() * 100; osc.connect(gain); gain.connect(ctx.destination); gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(0.02, now + 0.02); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15); osc.start(now); osc.stop(now + 0.15); } } else if (type === 'whoosh') { // Gentle breeze sound - soft and airy this.playBreeze(0.8, 0.05); } else if (type === 'ambient') { // Swell the ambient music for the reveal moment this.swellAmbientMusic(); } }, // Start beautiful ambient background music with spatial stereo // Optimized for MacBook Pro speakers and spatial audio startAmbientMusic() { if (!AudioSystem || !AudioSystem.ctx) return; if (this.ambientMusic) return; // Already playing const ctx = AudioSystem.ctx; const now = ctx.currentTime; // Master output with gentle fade in const masterGain = ctx.createGain(); masterGain.connect(ctx.destination); masterGain.gain.setValueAtTime(0, now); masterGain.gain.linearRampToValueAtTime(0.08, now + 4); // 4-second fade in const oscillators = []; const panners = []; // Chord voices with stereo positioning and subtle detuning // Doubled voices panned L/R create width and warmth const voices = [ { freq: 130.81, pan: 0, detune: -3, vol: 0.10 }, // C3 - center (foundation) { freq: 196.00, pan: -0.4, detune: 2, vol: 0.08 }, // G3 - slight left { freq: 196.00, pan: 0.4, detune: -2, vol: 0.08 }, // G3 - slight right { freq: 261.63, pan: -0.6, detune: 4, vol: 0.07 }, // C4 - left { freq: 261.63, pan: 0.6, detune: -4, vol: 0.07 }, // C4 - right { freq: 329.63, pan: -0.8, detune: 3, vol: 0.05 }, // E4 - far left { freq: 329.63, pan: 0.8, detune: -3, vol: 0.05 }, // E4 - far right { freq: 392.00, pan: -0.3, detune: 5, vol: 0.04 }, // G4 - mid left { freq: 392.00, pan: 0.3, detune: -5, vol: 0.04 }, // G4 - mid right ]; voices.forEach((voice) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); const panner = ctx.createStereoPanner(); osc.type = 'sine'; osc.frequency.value = voice.freq; osc.detune.value = voice.detune; // Slow wandering vibrato const lfo = ctx.createOscillator(); const lfoGain = ctx.createGain(); lfo.frequency.value = 0.12 + Math.random() * 0.1; lfoGain.gain.value = voice.freq * 0.003; lfo.connect(lfoGain); lfoGain.connect(osc.frequency); lfo.start(now); // Subtle pan drift for movement const panLfo = ctx.createOscillator(); const panLfoGain = ctx.createGain(); panLfo.frequency.value = 0.04 + Math.random() * 0.03; panLfoGain.gain.value = 0.12; panLfo.connect(panLfoGain); panLfoGain.connect(panner.pan); panLfo.start(now); panner.pan.setValueAtTime(voice.pan, now); osc.connect(gain); gain.connect(panner); panner.connect(masterGain); gain.gain.setValueAtTime(voice.vol, now); osc.start(now); oscillators.push(osc, lfo, panLfo); panners.push(panner); }); // High shimmer - twinkling stars panned wide const shimmerVoices = [ { freq: 1046.50, pan: -0.9, vol: 0.012 }, // C6 far left { freq: 1174.66, pan: 0.9, vol: 0.010 }, // D6 far right { freq: 1318.51, pan: -0.5, vol: 0.008 }, // E6 mid left { freq: 1567.98, pan: 0.5, vol: 0.007 }, // G6 mid right ]; shimmerVoices.forEach(voice => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); const panner = ctx.createStereoPanner(); osc.type = 'sine'; osc.frequency.value = voice.freq; // Gentle twinkling const ampLfo = ctx.createOscillator(); const ampLfoGain = ctx.createGain(); ampLfo.frequency.value = 0.6 + Math.random() * 0.4; ampLfoGain.gain.value = voice.vol * 0.4; ampLfo.connect(ampLfoGain); ampLfoGain.connect(gain.gain); ampLfo.start(now); // Slow pan wander const panLfo = ctx.createOscillator(); const panLfoGain = ctx.createGain(); panLfo.frequency.value = 0.025 + Math.random() * 0.02; panLfoGain.gain.value = 0.25; panLfo.connect(panLfoGain); panLfoGain.connect(panner.pan); panLfo.start(now); panner.pan.setValueAtTime(voice.pan, now); osc.connect(gain); gain.connect(panner); panner.connect(masterGain); gain.gain.setValueAtTime(voice.vol, now); osc.start(now); oscillators.push(osc, ampLfo, panLfo); panners.push(panner); }); // Warm sub-bass (center, subtle) const subOsc = ctx.createOscillator(); const subGain = ctx.createGain(); subOsc.type = 'sine'; subOsc.frequency.value = 65.41; // C2 subOsc.connect(subGain); subGain.connect(masterGain); subGain.gain.setValueAtTime(0.03, now); subOsc.start(now); oscillators.push(subOsc); this.ambientMusic = { oscillators, panners, masterGain, ctx }; }, // Swell the ambient music for dramatic reveal swellAmbientMusic() { if (!this.ambientMusic) return; const { masterGain, ctx } = this.ambientMusic; const now = ctx.currentTime; // Gentle swell up then back down masterGain.gain.linearRampToValueAtTime(0.1, now + 2); masterGain.gain.linearRampToValueAtTime(0.06, now + 5); }, // Stop ambient music with fade out stopAmbientMusic() { if (!this.ambientMusic) return; const { oscillators, masterGain, ctx } = this.ambientMusic; const now = ctx.currentTime; // Gentle fade out masterGain.gain.linearRampToValueAtTime(0, now + 2); // Stop all oscillators after fade setTimeout(() => { oscillators.forEach(osc => { try { osc.stop(); } catch(e) {} }); }, 2500); this.ambientMusic = null; }, // Play a gentle chime (multiple frequencies) playChime(frequencies, volume, duration) { if (!AudioSystem || !AudioSystem.ctx) return; const ctx = AudioSystem.ctx; const now = ctx.currentTime; frequencies.forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; osc.connect(gain); gain.connect(ctx.destination); // Staggered, gentle attack const delay = i * 0.08; gain.gain.setValueAtTime(0, now + delay); gain.gain.linearRampToValueAtTime(volume * (1 - i * 0.15), now + delay + 0.1); gain.gain.exponentialRampToValueAtTime(0.001, now + delay + duration); osc.start(now + delay); osc.stop(now + delay + duration); }); }, // Play a gentle ascending shimmer playShimmer(startFreq, endFreq, duration, volume) { if (!AudioSystem || !AudioSystem.ctx) return; const ctx = AudioSystem.ctx; const now = ctx.currentTime; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(startFreq, now); osc.frequency.exponentialRampToValueAtTime(endFreq, now + duration); osc.connect(gain); gain.connect(ctx.destination); gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(volume, now + duration * 0.3); gain.gain.linearRampToValueAtTime(0, now + duration); osc.start(now); osc.stop(now + duration); }, // Play a gentle breeze/whoosh sound playBreeze(duration, volume) { if (!AudioSystem || !AudioSystem.ctx) return; const ctx = AudioSystem.ctx; const now = ctx.currentTime; // Use multiple sine waves at different frequencies for a soft breeze [300, 450, 600, 800].forEach(freq => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq + Math.random() * 50; osc.connect(gain); gain.connect(ctx.destination); gain.gain.setValueAtTime(0, now); gain.gain.linearRampToValueAtTime(volume * 0.3, now + duration * 0.3); gain.gain.linearRampToValueAtTime(0, now + duration); osc.start(now); osc.stop(now + duration); }); }, // Create UI overlay createUI() { // Black overlay for fades this.overlay = document.createElement('div'); this.overlay.id = 'landing-overlay'; this.overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: black; z-index: 10000; opacity: 1; pointer-events: none; transition: opacity 0.5s ease; `; document.body.appendChild(this.overlay); // Skip hint this.skipHint = document.createElement('div'); this.skipHint.id = 'landing-skip'; this.skipHint.innerHTML = 'Press SPACE or CLICK to skip'; this.skipHint.style.cssText = ` position: fixed; bottom: 30px; right: 30px; color: rgba(255,255,255,0.5); font-family: 'Courier New', monospace; font-size: 14px; z-index: 10001; opacity: 0; transition: opacity 0.5s ease; `; document.body.appendChild(this.skipHint); // Title card this.titleCard = document.createElement('div'); this.titleCard.id = 'landing-title'; this.titleCard.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-family: 'Courier New', monospace; text-align: center; z-index: 10001; opacity: 0; transition: opacity 1s ease; `; document.body.appendChild(this.titleCard); }, // Remove UI removeUI() { if (this.overlay) this.overlay.remove(); if (this.skipHint) this.skipHint.remove(); if (this.titleCard) this.titleCard.remove(); this.overlay = null; this.skipHint = null; this.titleCard = null; }, // Initialize landing sequence start(landingPosition, planetName) { if (this.active) return; this.active = true; this.phase = 'fadeIn'; this.time = 0; this.phaseTime = 0; this.skipRequested = false; this.landingPos.copy(landingPosition); this.landingY = landingPosition.y; // Create UI this.createUI(); // Create spacecraft high above landing zone this.spacecraft = this.createSpacecraft(); this.spacecraft.position.set( landingPosition.x, landingPosition.y + 80, // Start high landingPosition.z ); this.spacecraft.rotation.y = Math.PI; // Face forward scene.add(this.spacecraft); // Hide player initially if (worldState.player) { worldState.player.visible = false; worldState.player.position.set( landingPosition.x, landingPosition.y + 0.8, landingPosition.z + 5 // Behind ship ramp ); } // Store original camera state this.originalCameraPos = camera.position.clone(); // Set initial cinematic camera - looking up at descending ship camera.position.set( landingPosition.x + 15, landingPosition.y + 5, landingPosition.z + 20 ); camera.lookAt(landingPosition.x, landingPosition.y + 40, landingPosition.z); // Set title if (this.titleCard) { this.titleCard.innerHTML = `
${planetName || 'UNKNOWN WORLD'}
First Contact
`; } // Hide game UI during cutscene document.querySelectorAll('#game-ui, #portrait-panel, #minimap-container, #hp-mp-panel').forEach(el => { if (el) el.style.opacity = '0'; }); // Add skip listeners this.skipHandler = (e) => { if (e.code === 'Space' || e.type === 'click') { this.skipRequested = true; } }; window.addEventListener('keydown', this.skipHandler); window.addEventListener('click', this.skipHandler); // Start engine sound this.playSound('engine'); // v12.12: Start ambient music for landing atmosphere if (typeof SpaceMusic !== 'undefined' && !SpaceMusic.isPlaying) { SpaceMusic.start(); } // Play special landing accent if (typeof SpaceMusic !== 'undefined') { SpaceMusic.playLandingAccent(); } console.log('🚀 Landing Sequence: Started cinematic intro'); }, // Update landing sequence update(deltaTime) { if (!this.active) return; this.time += deltaTime; this.phaseTime += deltaTime; // Handle skip if (this.skipRequested && this.phase !== 'fadeOut' && this.phase !== 'complete') { this.phase = 'fadeOut'; this.phaseTime = 0; if (this.overlay) this.overlay.style.opacity = '1'; } const dt = Math.min(deltaTime, 0.05); // Cap delta for stability // Update particles this.updateParticles(dt); // Phase-specific updates switch (this.phase) { case 'fadeIn': this.updateFadeIn(); break; case 'descent': this.updateDescent(dt); break; case 'landing': this.updateLanding(dt); break; case 'doorOpen': this.updateDoorOpen(dt); break; case 'emerge': this.updateEmerge(dt); break; case 'cameraReveal': this.updateCameraReveal(dt); break; case 'fadeOut': this.updateFadeOut(); break; } // Animate spacecraft elements if (this.spacecraft) { // Pulsing engine glow const pulse = 0.7 + Math.sin(this.time * 10) * 0.3; this.spacecraft.userData.engineGlows?.forEach(glow => { if (glow.material) glow.material.opacity = pulse; }); // Blinking nav lights const blink = Math.sin(this.time * 3) > 0; if (this.spacecraft.userData.tipL) { this.spacecraft.userData.tipL.material.emissiveIntensity = blink ? 1 : 0.3; } if (this.spacecraft.userData.tipR) { this.spacecraft.userData.tipR.material.emissiveIntensity = blink ? 0.3 : 1; } } }, // Update particles // v7.84: Uses pre-allocated _tempParticleVelocity to avoid clone() per particle per frame updateParticles(dt) { // Thruster particles for (let i = this.thrusterParticles.length - 1; i >= 0; i--) { const p = this.thrusterParticles[i]; // v7.84: Use temp vector instead of clone() this._tempParticleVelocity.copy(p.userData.velocity).multiplyScalar(dt); p.position.add(this._tempParticleVelocity); p.userData.velocity.y -= 5 * dt; // Gravity p.userData.life -= p.userData.decay * dt; p.material.opacity = p.userData.life * 0.9; p.scale.setScalar(p.userData.life * 1.5); if (p.userData.life <= 0) { scene.remove(p); this.thrusterParticles.splice(i, 1); } } // Dust particles for (let i = this.dustParticles.length - 1; i >= 0; i--) { const p = this.dustParticles[i]; // v7.84: Use temp vector instead of clone() this._tempParticleVelocity.copy(p.userData.velocity).multiplyScalar(dt); p.position.add(this._tempParticleVelocity); p.userData.velocity.y -= 3 * dt; p.userData.velocity.multiplyScalar(0.98); // Drag p.userData.life -= p.userData.decay * dt; p.material.opacity = p.userData.life * 0.6; p.scale.setScalar(1 + (1 - p.userData.life) * 2); if (p.userData.life <= 0) { scene.remove(p); this.dustParticles.splice(i, 1); } } }, updateFadeIn() { const progress = this.phaseTime / this.phaseDurations.fadeIn; if (this.overlay) { this.overlay.style.opacity = 1 - progress; } if (this.skipHint && progress > 0.5) { this.skipHint.style.opacity = '1'; } if (progress >= 1) { this.phase = 'descent'; this.phaseTime = 0; } }, updateDescent(dt) { const progress = this.phaseTime / this.phaseDurations.descent; const eased = 1 - Math.pow(1 - progress, 2); // Ease out if (this.spacecraft) { // Descend from high to landing position const startY = this.landingY + 80; const endY = this.landingY + 8; this.spacecraft.position.y = startY + (endY - startY) * eased; // Gentle sway this.spacecraft.rotation.z = Math.sin(this.time * 2) * 0.03; this.spacecraft.rotation.x = Math.sin(this.time * 1.5) * 0.02; // Deploy landing gear gradually const gearProgress = Math.max(0, (progress - 0.5) * 2); this.spacecraft.userData.landingGear?.forEach(gear => { gear.position.y = gear.userData.restY + (gear.userData.deployedY - gear.userData.restY) * gearProgress; }); // Spawn thruster particles if (Math.random() < 0.3) { [-2, 2].forEach(x => { const pos = new THREE.Vector3( this.spacecraft.position.x + x, this.spacecraft.position.y - 1, this.spacecraft.position.z - 2 ); const vel = new THREE.Vector3( (Math.random() - 0.5) * 2, -8 - Math.random() * 5, (Math.random() - 0.5) * 2 ); const particle = this.createThrusterParticle(pos, vel); scene.add(particle); this.thrusterParticles.push(particle); }); } } // Camera tracks descent const camProgress = eased; camera.position.set( this.landingPos.x + 15 - camProgress * 5, this.landingPos.y + 5 + (1 - camProgress) * 10, this.landingPos.z + 20 - camProgress * 10 ); camera.lookAt( this.landingPos.x, this.landingPos.y + 40 - camProgress * 35, this.landingPos.z ); if (progress >= 1) { this.phase = 'landing'; this.phaseTime = 0; this.playSound('landing'); } }, updateLanding(dt) { const progress = this.phaseTime / this.phaseDurations.landing; if (this.spacecraft) { // Final descent to ground const startY = this.landingY + 8; const endY = this.landingY + 2.5; if (progress < 0.3) { // Quick drop const dropProgress = progress / 0.3; this.spacecraft.position.y = startY + (endY - startY) * dropProgress; } else { // Settle with bounce const settleProgress = (progress - 0.3) / 0.7; const bounce = Math.sin(settleProgress * Math.PI * 3) * (1 - settleProgress) * 0.3; this.spacecraft.position.y = endY + bounce; } // Camera shake on impact if (progress < 0.3) { const shake = (1 - progress / 0.3) * 0.3; camera.position.x += (Math.random() - 0.5) * shake; camera.position.y += (Math.random() - 0.5) * shake; } // Spawn dust cloud on impact if (progress < 0.1) { for (let i = 0; i < 5; i++) { const pos = new THREE.Vector3( this.landingPos.x + (Math.random() - 0.5) * 8, this.landingY + 0.5, this.landingPos.z + (Math.random() - 0.5) * 8 ); const particle = this.createDustParticle(pos); scene.add(particle); this.dustParticles.push(particle); } } } // Move camera to side view const camEase = 1 - Math.pow(1 - progress, 2); camera.position.set( this.landingPos.x + 10, this.landingPos.y + 3, this.landingPos.z + 10 - camEase * 5 ); camera.lookAt( this.landingPos.x, this.landingPos.y + 3, this.landingPos.z ); if (progress >= 1) { this.phase = 'doorOpen'; this.phaseTime = 0; this.playSound('hydraulic'); } }, updateDoorOpen(dt) { const progress = this.phaseTime / this.phaseDurations.doorOpen; if (this.spacecraft && this.spacecraft.userData.ramp) { // Animate ramp opening (rotate from vertical to angled down) const rampAngle = Math.PI / 2 - progress * (Math.PI / 2 + 0.3); this.spacecraft.userData.ramp.rotation.x = rampAngle; // Turn on interior light if (this.spacecraft.userData.interiorLight && progress > 0.2) { this.spacecraft.userData.interiorLight.visible = true; this.spacecraft.userData.interiorLight.intensity = Math.min((progress - 0.2) * 3, 2); } } // Camera focuses on ramp camera.position.set( this.landingPos.x + 6, this.landingPos.y + 2, this.landingPos.z + 8 ); camera.lookAt( this.landingPos.x, this.landingPos.y + 2, this.landingPos.z + 3 ); if (progress >= 1) { this.phase = 'emerge'; this.phaseTime = 0; // Position player at top of ramp if (worldState.player) { worldState.player.position.set( this.landingPos.x, this.landingPos.y + 2, this.landingPos.z + 3 ); worldState.player.rotation.y = Math.PI; // Face outward } } }, updateEmerge(dt) { const progress = this.phaseTime / this.phaseDurations.emerge; if (worldState.player) { // Make player visible and walk down ramp worldState.player.visible = true; const startPos = new THREE.Vector3( this.landingPos.x, this.landingPos.y + 2, this.landingPos.z + 3 ); const endPos = new THREE.Vector3( this.landingPos.x, this.landingPos.y + 0.8, this.landingPos.z + 7 ); // Ease out movement const moveProgress = Math.min(progress * 1.2, 1); const eased = 1 - Math.pow(1 - moveProgress, 2); worldState.player.position.lerpVectors(startPos, endPos, eased); // Walking animation if (worldState.player.userData.animation) { worldState.player.userData.animation.state = 'walking'; worldState.player.userData.animation.walkCycle += dt * 8; } // Footstep sounds if (Math.sin(this.phaseTime * 6) > 0.9) { this.playSound('footstep'); } // Robot looks around if (worldState.player.userData.bones?.headGroup) { const lookPhase = this.phaseTime * 0.8; worldState.player.userData.bones.headGroup.rotation.y = Math.sin(lookPhase) * 0.4; worldState.player.userData.bones.headGroup.rotation.x = Math.sin(lookPhase * 0.7) * 0.1 - 0.1; } } // Camera follows robot, low angle hero shot const camProgress = Math.min(progress * 1.5, 1); camera.position.set( this.landingPos.x + 4 - camProgress * 2, this.landingPos.y + 1, this.landingPos.z + 10 + camProgress * 2 ); camera.lookAt( this.landingPos.x, this.landingPos.y + 1.5, this.landingPos.z + 5 ); if (progress >= 1) { this.phase = 'cameraReveal'; this.phaseTime = 0; this.playSound('whoosh'); this.playSound('ambient'); // Show title card if (this.titleCard) { this.titleCard.style.opacity = '1'; } // Stop walking animation if (worldState.player?.userData.animation) { worldState.player.userData.animation.state = 'idle'; } } }, updateCameraReveal(dt) { const progress = this.phaseTime / this.phaseDurations.cameraReveal; // Epic pullback and pan const eased = 1 - Math.pow(1 - progress, 3); // Strong ease out // Camera arcs around and pulls back const angle = eased * Math.PI * 0.6; const distance = 8 + eased * 25; const height = 2 + eased * 15; camera.position.set( this.landingPos.x + Math.sin(angle) * distance, this.landingPos.y + height, this.landingPos.z + Math.cos(angle) * distance ); // Look at a point between player and horizon const lookHeight = 1 + eased * 5; camera.lookAt( this.landingPos.x, this.landingPos.y + lookHeight, this.landingPos.z ); // Fade out title card if (this.titleCard && progress > 0.6) { this.titleCard.style.opacity = String(1 - (progress - 0.6) / 0.4); } // Robot turns to face camera if (worldState.player && progress > 0.3) { const turnProgress = (progress - 0.3) / 0.4; worldState.player.rotation.y = Math.PI + Math.min(turnProgress, 1) * (-angle); } if (progress >= 1) { this.phase = 'fadeOut'; this.phaseTime = 0; if (this.overlay) this.overlay.style.opacity = '0.5'; } }, updateFadeOut() { const progress = this.phaseTime / this.phaseDurations.fadeOut; // Quick fade if (this.overlay) { if (progress < 0.3) { this.overlay.style.opacity = String(0.5 + progress); } else { this.overlay.style.opacity = String(1 - (progress - 0.3) / 0.7); } } // Hide skip hint if (this.skipHint) { this.skipHint.style.opacity = '0'; } if (progress >= 1) { this.complete(); } }, // Complete and cleanup complete() { this.phase = 'complete'; this.active = false; // Remove event listeners if (this.skipHandler) { window.removeEventListener('keydown', this.skipHandler); window.removeEventListener('click', this.skipHandler); } // Clean up particles this.thrusterParticles.forEach(p => scene.remove(p)); this.dustParticles.forEach(p => scene.remove(p)); this.thrusterParticles = []; this.dustParticles = []; // Remove spacecraft (leave world ship) if (this.spacecraft) { scene.remove(this.spacecraft); this.spacecraft = null; } // Remove UI this.removeUI(); // Stop ambient music with gentle fade out this.stopAmbientMusic(); // Reset player to idle if (worldState.player) { worldState.player.visible = true; if (worldState.player.userData.animation) { worldState.player.userData.animation.state = 'idle'; } if (worldState.player.userData.bones?.headGroup) { worldState.player.userData.bones.headGroup.rotation.set(0, 0, 0); } } // Restore game UI document.querySelectorAll('#game-ui, #portrait-panel, #minimap-container, #hp-mp-panel').forEach(el => { if (el) el.style.opacity = '1'; }); // Reset camera for gameplay if (typeof updateCameraForGameplay === 'function') { updateCameraForGameplay(); } // Initialize RTS selection if (RTSSelection) { RTSSelection.init(); } console.log('🚀 Landing Sequence: Complete - Welcome to the world!'); }, // Check if sequence is running isActive() { return this.active; } }; // ============================================ // v6.93: TIME REWIND SYSTEM // Auto-saves snapshots every 30 seconds // Rewind to any point in your 12+ hour session // Persists to localStorage - survives refreshes // ============================================ const TimeRewind = { // Configuration SNAPSHOT_INTERVAL: 30000, // Auto-snapshot every 30 seconds MAX_SNAPSHOTS: 1440, // 12 hours worth at 30s intervals STORAGE_KEY: 'levi-time-rewind', // State snapshots: [], lastSnapshotTime: 0, isRewinding: false, selectedIndex: -1, // Initialize - load existing snapshots from storage // v8.0: Using SafeJSON for TimeRewind snapshots (8-Strategy Consensus Cycle 4) init() { const data = SafeJSON.fromLocalStorage(this.STORAGE_KEY, { snapshots: [] }); this.snapshots = data.snapshots || []; if (this.snapshots.length > 0) { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`⏪ TimeRewind: Loaded ${this.snapshots.length} snapshots from storage`); } // v6.93: Take immediate snapshot on init to capture current state setTimeout(() => { this.takeSnapshot('Session start - ' + new Date().toLocaleTimeString()); console.log('⏪ TimeRewind: Created initial snapshot'); }, 3000); // Wait 3s for game to fully initialize with galaxy this.updateUI(); }, // Take a snapshot of current game state takeSnapshot(label = null) { const snapshot = { id: Date.now(), timestamp: Date.now(), playtime: gameData.playtime, cycle: gameData.totalCycles, label: label || `Auto-save at ${this.formatTime(gameData.playtime)}`, // Full gameData clone gameData: JSON.parse(JSON.stringify(gameData)), // Galaxy state civilizations: civilizations.map(c => ({ id: c.id, x: c.x, y: c.y, z: c.z, name: c.name, biome: c.biome, visited: c.visited, orbital: c.orbital ? { ...c.orbital } : null })), // Physics state physics: { ...physicsParams }, // Current mode mode: mode, // Active planet activePlanetId: activeCiv?.id ?? null }; this.snapshots.push(snapshot); // Trim to max snapshots (keep most recent) while (this.snapshots.length > this.MAX_SNAPSHOTS) { this.snapshots.shift(); } this.saveToStorage(); this.updateUI(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`⏪ Snapshot taken: ${snapshot.label} (${this.snapshots.length} total)`); return snapshot; }, // Auto-snapshot called from animate loop update(time) { if (this.isRewinding) return; if (time - this.lastSnapshotTime >= this.SNAPSHOT_INTERVAL) { this.takeSnapshot(); this.lastSnapshotTime = time; } }, // Restore game to a specific snapshot restoreSnapshot(index) { if (index < 0 || index >= this.snapshots.length) { showNotification('Invalid snapshot index!', 'error'); return; } const snapshot = this.snapshots[index]; this.isRewinding = true; try { // Restore gameData Object.assign(gameData, snapshot.gameData); // Restore civilizations state snapshot.civilizations.forEach((savedCiv, i) => { if (civilizations[i]) { civilizations[i].x = savedCiv.x; civilizations[i].y = savedCiv.y; civilizations[i].z = savedCiv.z; civilizations[i].visited = savedCiv.visited; // v7.26: 8-STRATEGY CONSENSUS FIX - Restore biome data (was missing!) // This fixes the bug where all worlds become Industrial after time rewind if (savedCiv.biome) { civilizations[i].biome = savedCiv.biome; civilizations[i].biomeName = savedCiv.biomeName || (BIOMES[savedCiv.biome]?.name || 'Terra'); } if (savedCiv.orbital && civilizations[i].orbital) { Object.assign(civilizations[i].orbital, savedCiv.orbital); } // Update 3D mesh position and visibility const group = galaxyGroup?.children[i]; if (group) { group.position.set(savedCiv.x, savedCiv.y, savedCiv.z); const destroyed = savedCiv.orbital?.destroyed; const escaped = savedCiv.orbital?.escaped; group.visible = !(destroyed || escaped); } } }); // Restore physics Object.assign(physicsParams, snapshot.physics); // Save restored state saveGameData(); // Remove all snapshots after this one (branch off) this.snapshots = this.snapshots.slice(0, index + 1); this.saveToStorage(); showNotification(`⏪ Rewound to: ${snapshot.label}`, 'success'); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`⏪ Restored snapshot from index ${index}`); } catch (e) { // v8.26: Enhanced error message with snapshot context console.error(`[TimeRewind] v8.26: Restore failed for snapshot at index ${index}. Snapshot label: "${snapshot?.label || 'unknown'}". Error:`, e.message || e); showNotification('Rewind failed! Check console for details.', 'error'); } this.isRewinding = false; this.updateUI(); }, // Rewind by N steps rewindSteps(steps = 1) { const targetIndex = this.snapshots.length - 1 - steps; if (targetIndex >= 0) { this.restoreSnapshot(targetIndex); } else { showNotification('No earlier snapshots available!', 'error'); } }, // Quick rewind to last snapshot quickRewind() { if (this.snapshots.length >= 2) { this.restoreSnapshot(this.snapshots.length - 2); } else { showNotification('Need at least 2 snapshots to rewind!', 'error'); } }, // Manual save point with label createSavePoint(label) { const snapshot = this.takeSnapshot(label || `Manual save - ${new Date().toLocaleTimeString()}`); showNotification(`💾 Save point created: ${snapshot.label}`, 'success'); }, // Save snapshots to localStorage saveToStorage() { try { // Only save essential data to reduce storage size const toSave = { version: '1.0', savedAt: Date.now(), snapshots: this.snapshots }; localStorage.setItem(this.STORAGE_KEY, JSON.stringify(toSave)); } catch (e) { console.warn('TimeRewind: Storage save failed (quota?)', e); // Aggressively reduce snapshots on quota error if (this.snapshots.length > 5) { // Keep only the 5 most recent snapshots this.snapshots = this.snapshots.slice(-5); console.log('⏪ TimeRewind: Reduced to 5 snapshots to free storage'); try { const reduced = { version: '1.0', savedAt: Date.now(), snapshots: this.snapshots }; localStorage.setItem(this.STORAGE_KEY, JSON.stringify(reduced)); } catch (e2) { // If still failing, clear entirely console.warn('⏪ TimeRewind: Clearing all snapshots due to quota'); this.snapshots = []; localStorage.removeItem(this.STORAGE_KEY); } } } }, // Export all snapshots to file exportSnapshots() { if (this.snapshots.length === 0) { showNotification('No snapshots to export!', 'error'); return; } const data = { version: '1.0', exportedAt: Date.now(), totalPlaytime: gameData.playtime, snapshotCount: this.snapshots.length, snapshots: this.snapshots }; const json = JSON.stringify(data); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `levi-timeline-${new Date().toISOString().split('T')[0]}.json`; link.click(); URL.revokeObjectURL(url); const sizeMB = (json.length / 1024 / 1024).toFixed(2); showNotification(`📁 Exported ${this.snapshots.length} snapshots (${sizeMB}MB)`, 'success'); }, // v8.31: Use ErrorRecovery.safeJSONParse for safer snapshot import importSnapshots(file) { const reader = new FileReader(); reader.onload = (e) => { const data = ErrorRecovery.safeJSONParse(e.target.result, null); if (!data) { showNotification('Import failed: Invalid JSON format', 'error'); return; } if (!data.snapshots || !Array.isArray(data.snapshots)) { showNotification('Import failed: Invalid timeline file', 'error'); return; } this.snapshots = data.snapshots; this.saveToStorage(); this.updateUI(); showNotification(`Imported ${this.snapshots.length} snapshots`, 'success'); }; reader.readAsText(file); }, // Clear all snapshots clearSnapshots() { if (confirm('Clear all time rewind data? This cannot be undone.')) { this.snapshots = []; localStorage.removeItem(this.STORAGE_KEY); this.updateUI(); showNotification('Timeline cleared', 'info'); } }, // Format playtime formatTime(seconds) { const hrs = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); if (hrs > 0) return `${hrs}h ${mins}m`; return `${mins}m`; }, // Update the rewind UI updateUI() { const countEl = document.getElementById('rewind-count'); const sliderEl = document.getElementById('rewind-slider'); const labelEl = document.getElementById('rewind-label'); if (countEl) { countEl.textContent = this.snapshots.length; } if (sliderEl) { sliderEl.max = Math.max(0, this.snapshots.length - 1); sliderEl.value = this.snapshots.length - 1; } if (labelEl && this.snapshots.length > 0) { const latest = this.snapshots[this.snapshots.length - 1]; labelEl.textContent = latest.label; } }, // Show rewind modal with timeline showModal() { let modal = document.getElementById('rewind-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'rewind-modal'; modal.innerHTML = `

⏪ Time Rewind

Select a point to rewind to. All progress after that point will be lost.

`; document.body.appendChild(modal); } // Populate timeline const timeline = document.getElementById('rewind-timeline'); if (timeline) { if (this.snapshots.length === 0) { timeline.innerHTML = '

No snapshots yet. Play for 30 seconds to create the first auto-save.

'; } else { timeline.innerHTML = this.snapshots.slice().reverse().map((snap, reverseIdx) => { const idx = this.snapshots.length - 1 - reverseIdx; const isLatest = idx === this.snapshots.length - 1; const civCount = snap.civilizations.filter(c => !c.orbital?.destroyed && !c.orbital?.escaped).length; const date = new Date(snap.timestamp); return `
${snap.label}
${date.toLocaleTimeString()} - Playtime: ${this.formatTime(snap.playtime)}
${civCount}
planets
`; }).join(''); } } } }; // Global function to show rewind modal function showRewindModal() { TimeRewind.showModal(); } // Quick rewind function for keyboard shortcut function quickRewind() { TimeRewind.quickRewind(); } // ============================================ // v6.56: CIVILIZATION GENESIS ENGINE // 8-Agent Consensus Implementation // Drop a seed, watch civilizations emerge from 4 rules: // 1. Attract Resources // 2. Claim Territory // 3. Reproduce // 4. Die // ============================================ const GENESIS_CONFIG = { // Rule 1: Attract Resources RESOURCE_SENSE_RADIUS: 60, RESOURCE_ATTRACTION_STRENGTH: 0.8, RESOURCE_GATHER_RATE: 0.5, RESOURCE_SPAWN_RATE: 0.03, MAX_RESOURCES: 100, // Rule 2: Claim Territory TERRITORY_RADIUS: 8, TERRITORY_BUILD_RATE: 2, TERRITORY_DECAY_RATE: 0.1, KINSHIP_BONUS: 0.3, // Reduced repulsion for family // Rule 3: Reproduce REPRODUCTION_THRESHOLD: 80, REPRODUCTION_COST: 40, REPRODUCTION_COOLDOWN: 100, // ticks MIN_REPRODUCTION_AGE: 50, // Rule 4: Die DEATH_AGE: 800, STARVATION_THRESHOLD: 5, ENERGY_DECAY_RATE: 0.15, // Performance MAX_ENTITIES: 500, WORLD_RADIUS: 150, CELL_SIZE: 15, // Spatial hash grid // Visual FACTION_COLORS: [0x66aaff, 0x66ff88, 0xff8866, 0xaa66ff, 0xffff66, 0xff66aa] }; // Genesis State let genesisState = { active: false, paused: false, speed: 1, tick: 0, entities: [], resources: [], territory: {}, // "x,z" -> { owner, lineage, strength } factions: {}, nextId: 1, nextFactionId: 0, events: [], stats: { peakPopulation: 0, totalFactions: 0, totalWars: 0, totalDeaths: 0 }, // Three.js objects entityMeshes: null, resourceMeshes: null, territoryMesh: null, genesisScene: null, previousMode: 'galaxy', previousCameraPos: null }; // Spatial hash grid for O(1) neighbor queries const GenesisSpatialGrid = { cells: new Map(), getKey(x, z) { const cx = Math.floor(x / GENESIS_CONFIG.CELL_SIZE); const cz = Math.floor(z / GENESIS_CONFIG.CELL_SIZE); return `${cx},${cz}`; }, clear() { this.cells.clear(); }, rebuild(entities) { this.cells.clear(); entities.forEach((entity, idx) => { if (entity.alive) { const key = this.getKey(entity.x, entity.z); if (!this.cells.has(key)) this.cells.set(key, []); this.cells.get(key).push(idx); } }); }, getNearby(x, z, radius) { const results = []; const cellRadius = Math.ceil(radius / GENESIS_CONFIG.CELL_SIZE); const cx = Math.floor(x / GENESIS_CONFIG.CELL_SIZE); const cz = Math.floor(z / GENESIS_CONFIG.CELL_SIZE); const radiusSq = radius * radius; for (let dx = -cellRadius; dx <= cellRadius; dx++) { for (let dz = -cellRadius; dz <= cellRadius; dz++) { const cell = this.cells.get(`${cx + dx},${cz + dz}`); if (!cell) continue; for (const idx of cell) { const entity = genesisState.entities[idx]; if (!entity || !entity.alive) continue; const distSq = (entity.x - x) ** 2 + (entity.z - z) ** 2; if (distSq <= radiusSq) { results.push({ idx, entity, distSq }); } } } } return results.sort((a, b) => a.distSq - b.distSq); } }; // Create a new entity function createGenesisEntity(x, z, parentLineage = null, generation = 0) { const id = genesisState.nextId++; const lineage = parentLineage || id; // Determine or create faction let factionId = null; if (parentLineage) { // Find parent's faction for (const [fid, faction] of Object.entries(genesisState.factions)) { if (faction.lineage === parentLineage) { factionId = parseInt(fid); break; } } } // Create new faction if needed (tribes emerge!) if (factionId === null && generation >= 2) { factionId = genesisState.nextFactionId++; const colorIdx = factionId % GENESIS_CONFIG.FACTION_COLORS.length; genesisState.factions[factionId] = { id: factionId, lineage: lineage, color: GENESIS_CONFIG.FACTION_COLORS[colorIdx], name: `Tribe ${factionId + 1}`, founded: genesisState.tick, population: 0 }; genesisState.stats.totalFactions++; logGenesisEvent(`🏛️ ${genesisState.factions[factionId].name} founded!`); } const entity = { id, x, z, energy: 50, age: 0, lineage, factionId, generation, speed: 1 + (Math.random() - 0.5) * 0.3, // Slight variation territoryStrength: 0, lastReproduction: 0, alive: true, state: 'wandering' // wandering, gathering, claiming, fighting }; genesisState.entities.push(entity); if (factionId !== null && genesisState.factions[factionId]) { genesisState.factions[factionId].population++; } return entity; } // Spawn a resource function spawnGenesisResource() { if (genesisState.resources.length >= GENESIS_CONFIG.MAX_RESOURCES) return; const angle = Math.random() * Math.PI * 2; const dist = Math.random() * GENESIS_CONFIG.WORLD_RADIUS * 0.9; genesisState.resources.push({ x: Math.cos(angle) * dist, z: Math.sin(angle) * dist, amount: 20 + Math.random() * 30 }); } // RULE 1: Attract Resources function applyAttractionRule(entity, idx) { // Find nearest resource let nearest = null; let nearestDistSq = Infinity; for (const resource of genesisState.resources) { if (resource.amount <= 0) continue; const dx = resource.x - entity.x; const dz = resource.z - entity.z; const distSq = dx * dx + dz * dz; if (distSq < nearestDistSq && distSq < GENESIS_CONFIG.RESOURCE_SENSE_RADIUS ** 2) { nearestDistSq = distSq; nearest = resource; } } if (nearest) { const dist = Math.sqrt(nearestDistSq); if (dist > 2) { // Move toward resource const dx = nearest.x - entity.x; const dz = nearest.z - entity.z; entity.x += (dx / dist) * entity.speed * GENESIS_CONFIG.RESOURCE_ATTRACTION_STRENGTH; entity.z += (dz / dist) * entity.speed * GENESIS_CONFIG.RESOURCE_ATTRACTION_STRENGTH; entity.state = 'gathering'; } else { // Gather resource const gather = Math.min(GENESIS_CONFIG.RESOURCE_GATHER_RATE, nearest.amount); entity.energy += gather; nearest.amount -= gather; } } else { // Wander randomly entity.x += (Math.random() - 0.5) * entity.speed * 0.5; entity.z += (Math.random() - 0.5) * entity.speed * 0.5; entity.state = 'wandering'; } // Keep in bounds - v8.08: use squared distance for comparison const distSq = entity.x * entity.x + entity.z * entity.z; const radiusSq = GENESIS_CONFIG.WORLD_RADIUS * GENESIS_CONFIG.WORLD_RADIUS; if (distSq > radiusSq) { // Only compute sqrt when actually out of bounds const dist = Math.sqrt(distSq); entity.x *= GENESIS_CONFIG.WORLD_RADIUS / dist; entity.z *= GENESIS_CONFIG.WORLD_RADIUS / dist; } } // RULE 2: Claim Territory function applyTerritoryRule(entity, idx) { const key = `${Math.floor(entity.x / 5)},${Math.floor(entity.z / 5)}`; if (!genesisState.territory[key]) { genesisState.territory[key] = { owner: entity.id, lineage: entity.lineage, strength: 0 }; } const tile = genesisState.territory[key]; const sameLineage = tile.lineage === entity.lineage; if (sameLineage) { // Strengthen owned territory tile.strength = Math.min(tile.strength + GENESIS_CONFIG.TERRITORY_BUILD_RATE, 100); entity.territoryStrength = tile.strength; } else { // Contest foreign territory (WAR EMERGES!) tile.strength -= GENESIS_CONFIG.TERRITORY_BUILD_RATE; entity.energy -= 1; // Fighting costs energy entity.state = 'fighting'; // Check for war declaration if (entity.factionId !== null && tile.lineage) { for (const [fid, faction] of Object.entries(genesisState.factions)) { if (faction.lineage === tile.lineage && parseInt(fid) !== entity.factionId) { // War! genesisState.stats.totalWars++; } } } if (tile.strength <= 0) { // Territory conquered! tile.owner = entity.id; tile.lineage = entity.lineage; tile.strength = 1; } } } // RULE 3: Reproduce function applyReproductionRule(entity, idx) { const ticksSinceRepro = genesisState.tick - entity.lastReproduction; if (entity.energy >= GENESIS_CONFIG.REPRODUCTION_THRESHOLD && entity.age >= GENESIS_CONFIG.MIN_REPRODUCTION_AGE && ticksSinceRepro >= GENESIS_CONFIG.REPRODUCTION_COOLDOWN && genesisState.entities.filter(e => e.alive).length < GENESIS_CONFIG.MAX_ENTITIES) { // Check density - don't reproduce if too crowded const nearby = GenesisSpatialGrid.getNearby(entity.x, entity.z, 10); if (nearby.length < 8) { // Spawn offspring const offsetAngle = Math.random() * Math.PI * 2; const offsetDist = 3 + Math.random() * 3; const childX = entity.x + Math.cos(offsetAngle) * offsetDist; const childZ = entity.z + Math.sin(offsetAngle) * offsetDist; createGenesisEntity(childX, childZ, entity.lineage, entity.generation + 1); entity.energy -= GENESIS_CONFIG.REPRODUCTION_COST; entity.lastReproduction = genesisState.tick; } } } // RULE 4: Die function applyDeathRule(entity, idx) { // Age entity.age++; // Energy decay entity.energy -= GENESIS_CONFIG.ENERGY_DECAY_RATE; // Death conditions let shouldDie = false; let cause = ''; if (entity.age >= GENESIS_CONFIG.DEATH_AGE) { shouldDie = true; cause = 'old age'; } else if (entity.energy <= GENESIS_CONFIG.STARVATION_THRESHOLD) { shouldDie = true; cause = 'starvation'; } if (shouldDie) { entity.alive = false; genesisState.stats.totalDeaths++; if (entity.factionId !== null && genesisState.factions[entity.factionId]) { genesisState.factions[entity.factionId].population--; if (genesisState.factions[entity.factionId].population <= 0) { logGenesisEvent(`💀 ${genesisState.factions[entity.factionId].name} has fallen!`); } } } } // Main simulation update function updateGenesisSimulation(dt) { if (!genesisState.active || genesisState.paused) return; const speed = genesisState.speed; for (let s = 0; s < speed; s++) { genesisState.tick++; // Rebuild spatial grid GenesisSpatialGrid.rebuild(genesisState.entities); // v8.20: Use for loop instead of forEach for entity rule application const entitiesLen = genesisState.entities.length; for (let idx = 0; idx < entitiesLen; idx++) { const entity = genesisState.entities[idx]; if (!entity.alive) continue; applyAttractionRule(entity, idx); applyTerritoryRule(entity, idx); applyReproductionRule(entity, idx); applyDeathRule(entity, idx); } // Remove depleted resources genesisState.resources = genesisState.resources.filter(r => r.amount > 0); // Spawn new resources if (Math.random() < GENESIS_CONFIG.RESOURCE_SPAWN_RATE) { spawnGenesisResource(); } // Decay territory for (const key in genesisState.territory) { genesisState.territory[key].strength -= GENESIS_CONFIG.TERRITORY_DECAY_RATE; if (genesisState.territory[key].strength <= 0) { delete genesisState.territory[key]; } } } // Update stats const aliveCount = genesisState.entities.filter(e => e.alive).length; if (aliveCount > genesisState.stats.peakPopulation) { genesisState.stats.peakPopulation = aliveCount; } // Update UI updateGenesisUI(); // Update 3D rendering updateGenesisRendering(); } // Update UI display function updateGenesisUI() { const alive = genesisState.entities.filter(e => e.alive).length; const factions = Object.values(genesisState.factions).filter(f => f.population > 0).length; const wars = genesisState.stats.totalWars; const age = Math.floor(genesisState.tick / 10); document.getElementById('genesis-population').textContent = alive; document.getElementById('genesis-factions').textContent = factions; document.getElementById('genesis-age').textContent = age; document.getElementById('genesis-wars').textContent = wars; } // Initialize Genesis 3D rendering function initGenesisRendering() { // Create instanced mesh for entities const entityGeometry = new THREE.SphereGeometry(0.5, 8, 8); const entityMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0x333333, metalness: 0.3, roughness: 0.7 }); genesisState.entityMeshes = new THREE.InstancedMesh( entityGeometry, entityMaterial, GENESIS_CONFIG.MAX_ENTITIES ); genesisState.entityMeshes.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // Color buffer for per-instance colors const colors = new Float32Array(GENESIS_CONFIG.MAX_ENTITIES * 3); genesisState.entityMeshes.instanceColor = new THREE.InstancedBufferAttribute(colors, 3); scene.add(genesisState.entityMeshes); // Create resource markers const resourceGeometry = new THREE.ConeGeometry(0.3, 0.8, 4); const resourceMaterial = new THREE.MeshStandardMaterial({ color: 0x44ff44, emissive: 0x114411 }); genesisState.resourceMeshes = new THREE.InstancedMesh( resourceGeometry, resourceMaterial, GENESIS_CONFIG.MAX_RESOURCES ); scene.add(genesisState.resourceMeshes); // Create ground plane for territory visualization const groundGeometry = new THREE.PlaneGeometry( GENESIS_CONFIG.WORLD_RADIUS * 2.5, GENESIS_CONFIG.WORLD_RADIUS * 2.5, 64, 64 ); const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x222211, roughness: 1, metalness: 0 }); genesisState.territoryMesh = new THREE.Mesh(groundGeometry, groundMaterial); genesisState.territoryMesh.rotation.x = -Math.PI / 2; genesisState.territoryMesh.position.y = -0.5; scene.add(genesisState.territoryMesh); } // v8.20: Pre-allocated dummy object and color for genesis rendering const _genesisDummy = typeof THREE !== 'undefined' ? new THREE.Object3D() : null; const _genesisColor = typeof THREE !== 'undefined' ? new THREE.Color() : null; // Update 3D rendering function updateGenesisRendering() { if (!genesisState.entityMeshes) return; const dummy = _genesisDummy; const color = _genesisColor; let visibleCount = 0; // v8.20: Use for loop instead of forEach for entity rendering const entitiesLen = genesisState.entities.length; for (let idx = 0; idx < entitiesLen; idx++) { const entity = genesisState.entities[idx]; if (!entity.alive) continue; dummy.position.set(entity.x, 0.5, entity.z); dummy.scale.setScalar(0.5 + (entity.energy / 100) * 0.3); dummy.updateMatrix(); genesisState.entityMeshes.setMatrixAt(visibleCount, dummy.matrix); // Color based on faction if (entity.factionId !== null && genesisState.factions[entity.factionId]) { color.setHex(genesisState.factions[entity.factionId].color); } else { color.setHex(0xaaaaaa); } genesisState.entityMeshes.instanceColor.setXYZ(visibleCount, color.r, color.g, color.b); visibleCount++; } genesisState.entityMeshes.count = visibleCount; genesisState.entityMeshes.instanceMatrix.needsUpdate = true; genesisState.entityMeshes.instanceColor.needsUpdate = true; // v8.20: Use for loop for resource rendering let resourceCount = 0; const resourcesLen = genesisState.resources.length; for (let idx = 0; idx < resourcesLen; idx++) { const resource = genesisState.resources[idx]; if (resource.amount <= 0) continue; dummy.position.set(resource.x, 0.4, resource.z); dummy.scale.setScalar(0.5 + (resource.amount / 50) * 0.3); dummy.updateMatrix(); genesisState.resourceMeshes.setMatrixAt(resourceCount, dummy.matrix); resourceCount++; } genesisState.resourceMeshes.count = resourceCount; genesisState.resourceMeshes.instanceMatrix.needsUpdate = true; } // Cleanup genesis rendering function cleanupGenesisRendering() { if (genesisState.entityMeshes) { scene.remove(genesisState.entityMeshes); genesisState.entityMeshes.geometry.dispose(); genesisState.entityMeshes.material.dispose(); genesisState.entityMeshes = null; } if (genesisState.resourceMeshes) { scene.remove(genesisState.resourceMeshes); genesisState.resourceMeshes.geometry.dispose(); genesisState.resourceMeshes.material.dispose(); genesisState.resourceMeshes = null; } if (genesisState.territoryMesh) { scene.remove(genesisState.territoryMesh); genesisState.territoryMesh.geometry.dispose(); genesisState.territoryMesh.material.dispose(); genesisState.territoryMesh = null; } } // Toggle Genesis Mode function toggleGenesisMode() { if (mode === 'genesis') { exitGenesisMode(); } else { enterGenesisMode(); } } // Enter Genesis Mode function enterGenesisMode() { // Save current state genesisState.previousMode = mode; genesisState.previousCameraPos = camera.position.clone(); setMode('genesis'); // v8.27: Use setMode() for state validation genesisState.active = true; genesisState.paused = false; // Hide other UI document.querySelector('.hud-top').style.display = 'none'; document.getElementById('rpg-ui')?.classList.remove('visible'); // Show Genesis UI document.getElementById('genesis-hud').classList.add('active'); document.getElementById('genesis-controls-panel').classList.add('active'); document.getElementById('genesis-cursor').classList.add('active'); document.getElementById('genesis-button').classList.add('active'); document.getElementById('genesis-event-log').classList.add('active'); // Setup camera for top-down view camera.position.set(0, 200, 100); camera.lookAt(0, 0, 0); // Initialize rendering initGenesisRendering(); // Spawn initial resources for (let i = 0; i < 30; i++) { spawnGenesisResource(); } // Add click listener for dropping seeds renderer.domElement.addEventListener('click', handleGenesisClick); logGenesisEvent('🧬 Genesis Mode activated - Click to drop seed!'); showNotification('Genesis Mode - Click to drop seed particle', '#ffa500'); } // Exit Genesis Mode // v8.28: Added ResourceManager cleanup and timer clearing function exitGenesisMode() { genesisState.active = false; mode = genesisState.previousMode || 'galaxy'; // Remove click listener renderer.domElement.removeEventListener('click', handleGenesisClick); // Hide Genesis UI document.getElementById('genesis-hud').classList.remove('active'); document.getElementById('genesis-controls-panel').classList.remove('active'); document.getElementById('genesis-cursor').classList.remove('active'); document.getElementById('genesis-button').classList.remove('active'); document.getElementById('genesis-event-log').classList.remove('active'); // Show main UI document.querySelector('.hud-top').style.display = 'flex'; // Cleanup rendering cleanupGenesisRendering(); // v8.28: Clear genesis-related timers if (typeof TimerRegistry !== 'undefined') { TimerRegistry.clearInterval('genesis-update'); TimerRegistry.clearInterval('genesis-spawn'); } // Reset state genesisState.entities = []; genesisState.resources = []; genesisState.territory = {}; genesisState.factions = {}; genesisState.tick = 0; genesisState.nextId = 1; genesisState.nextFactionId = 0; genesisState.events = []; // Restore camera if (genesisState.previousCameraPos) { camera.position.copy(genesisState.previousCameraPos); } showNotification('Exited Genesis Mode', '#0ff'); } // Drop seed at position function dropGenesisSeed(worldX, worldZ) { const entity = createGenesisEntity(worldX, worldZ, null, 0); entity.energy = 100; // Full energy for seed logGenesisEvent(`🌱 Seed planted at (${Math.round(worldX)}, ${Math.round(worldZ)})`); showNotification('Seed planted! Watch civilization emerge...', '#ffa500'); } // Genesis click handler function handleGenesisClick(event) { if (mode !== 'genesis') return; // Convert screen to world coordinates const rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; raycaster.setFromCamera(mouse, camera); // Intersect with ground plane const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0); const intersection = new THREE.Vector3(); raycaster.ray.intersectPlane(plane, intersection); if (intersection) { dropGenesisSeed(intersection.x, intersection.z); } } // Speed control function setGenesisSpeed(speed) { genesisState.speed = speed; genesisState.paused = (speed === 0); document.querySelectorAll('.genesis-speed-btn').forEach(btn => { btn.classList.toggle('active', parseInt(btn.dataset.speed) === speed); }); showNotification(speed === 0 ? 'Genesis Paused' : `Genesis ${speed}x Speed`, '#ffa500'); } // Divine interventions function triggerGenesisBless() { const alive = genesisState.entities.filter(e => e.alive); if (alive.length === 0) { showNotification('No entities to bless!', '#ff6644'); return; } // Bless random entities with energy boost const tooBless = Math.min(10, alive.length); for (let i = 0; i < tooBless; i++) { const entity = alive[Math.floor(Math.random() * alive.length)]; entity.energy = Math.min(entity.energy + 30, 150); } logGenesisEvent('✨ Divine blessing bestowed!'); showNotification('Blessing bestowed! Entities energized.', '#ffd700'); } function triggerGenesisDisaster() { const alive = genesisState.entities.filter(e => e.alive); if (alive.length === 0) { showNotification('No entities to afflict!', '#ff6644'); return; } // Random disaster affects area const centerX = (Math.random() - 0.5) * GENESIS_CONFIG.WORLD_RADIUS; const centerZ = (Math.random() - 0.5) * GENESIS_CONFIG.WORLD_RADIUS; const radius = 30 + Math.random() * 20; // v8.08: forEach to for loop + squared distance optimization let affected = 0; const radiusSq = radius * radius; for (let i = 0; i < alive.length; i++) { const entity = alive[i]; const dx = entity.x - centerX; const dz = entity.z - centerZ; const distSq = dx * dx + dz * dz; if (distSq < radiusSq) { entity.energy -= 40; if (entity.energy <= 0) entity.alive = false; affected++; } } logGenesisEvent(`💥 Disaster struck! ${affected} affected.`); showNotification(`Disaster! ${affected} entities affected.`, '#ff4444'); } // Event logging function logGenesisEvent(message) { const ageStr = Math.floor(genesisState.tick / 10); genesisState.events.unshift({ tick: genesisState.tick, message }); // Keep last 50 events if (genesisState.events.length > 50) { genesisState.events.pop(); } // Update log display const logEl = document.getElementById('genesis-event-log'); if (logEl) { logEl.innerHTML = genesisState.events.slice(0, 15).map(e => `
[${Math.floor(e.tick/10)}]${e.message}
` ).join(''); } } // Genesis cursor tracking document.addEventListener('mousemove', (e) => { if (mode !== 'genesis') return; const cursor = document.getElementById('genesis-cursor'); if (cursor) { cursor.style.left = (e.clientX - 25) + 'px'; cursor.style.top = (e.clientY - 25) + 'px'; } }); // v5.18: P2P Spectator Streaming System let p2pStreaming = { peer: null, peerId: null, isHost: true, // Host (player) or spectator connections: [], // Active spectator connections hostConnection: null, // Connection to host (when spectator) qrCodeVisible: false, lastFrameTime: 0, frameInterval: 100, // Send frame every 100ms (10 FPS for efficiency) spectatorCount: 0, streamCanvas: null, // Offscreen canvas for capturing isSpectating: false, spectatorData: null, // Received data when spectating // v5.20: Simple stream controls streamPaused: false, // When true, camera stops syncing hostGameMode: null // Track host's current mode }; // v7.0: Public World Manager - First-Person-Is-Host System const PublicWorldManager = { REGISTRY_URL: 'https://raw.githubusercontent.com/kody-w/localFirstTools/main/data/public-worlds/registry.json', registry: null, currentWorld: null, isHost: false, hostPeerId: null, participants: [], hostQueue: [], myPeer: null, hostConnection: null, hostPeer: null, // v8.36: Auto-reconnect system (8-Strategy Consensus Round 4) reconnectAttempts: 0, maxReconnectAttempts: 3, reconnectDelays: [1000, 2000, 4000], // Exponential backoff: 1s, 2s, 4s reconnectTimer: null, async loadRegistry() { try { const response = await fetch(this.REGISTRY_URL + '?t=' + Date.now()); this.registry = await response.json(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[PUBLIC WORLDS] Loaded ${this.registry.worlds.length} worlds`); return this.registry; } catch (error) { console.error('[PUBLIC WORLDS] Failed to load registry:', error); return null; } }, async joinWorld(worldId, hostPeerId = null) { if (!this.registry) await this.loadRegistry(); const worldConfig = this.registry?.worlds.find(w => w.id === worldId); if (!worldConfig) { showNotification('World not found in registry', 'error'); return false; } document.getElementById('loading').style.display = 'flex'; document.getElementById('loading').innerHTML = `
🌍
Loading Public World...
${worldConfig.name}
${worldConfig.description}
`; try { const seedResponse = await fetch(worldConfig.seedUrl + '?t=' + Date.now()); const seedData = await seedResponse.json(); this.currentWorld = worldId; if (hostPeerId) { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[PUBLIC WORLDS] Joining existing host: ${hostPeerId}`); await this.connectToHost(hostPeerId, seedData); } else { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[PUBLIC WORLDS] No host specified - BECOMING HOST`); await this.becomeHost(worldId, seedData); } return true; } catch (error) { console.error('[PUBLIC WORLDS] Failed to load world:', error); showNotification('Failed to load world: ' + error.message, 'error'); document.getElementById('loading').style.display = 'none'; return false; } }, async becomeHost(worldId, seedData) { this.isHost = true; this.currentWorld = worldId; this.hostPeer = new Peer(`levi-world-${worldId}-${Date.now()}`); this.hostPeer.on('open', (id) => { this.hostPeerId = id; // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[HOST] Now hosting world ${worldId} as ${id}`); this.applySeedData(seedData); this.hostQueue = [id]; setTimeout(() => { this.showHostQRModal(worldId, id); showNotification(`🌍 HOSTING: ${seedData.config.name}`, 'success'); // v7.1: Show world chat toggle when multiplayer is active if (window.WorldChat) WorldChat.showToggle(); }, 1000); }); this.hostPeer.on('connection', (conn) => { this.handleNewParticipant(conn); }); this.hostPeer.on('error', (err) => { console.error('[HOST] Peer error:', err); showNotification('Host error: ' + err.type, 'error'); }); }, async connectToHost(hostPeerId, seedData) { this.isHost = false; this.hostPeerId = hostPeerId; this.myPeer = new Peer(); this.myPeer.on('open', (myId) => { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[JOINER] My peer ID: ${myId}`); const conn = this.myPeer.connect(hostPeerId, { reliable: true }); conn.on('open', () => { // v8.36: Reset reconnect attempts on successful connection this.reconnectAttempts = 0; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[JOINER] Connected to host!`); conn.send({ type: 'REQUEST_STATE' }); conn.send({ type: 'JOIN_QUEUE', peerId: myId }); showNotification(`🌍 Joined ${seedData.config.name}!`, 'success'); this.applySeedData(seedData); // v7.1: Show world chat toggle when multiplayer is active if (window.WorldChat) { WorldChat.showToggle(); WorldChat.addSystemMessage(`Joined ${seedData.config.name}!`); } }); conn.on('data', (data) => { this.handleHostMessage(data, seedData); }); conn.on('close', () => { console.log('[JOINER] Host disconnected!'); // v8.36: Attempt auto-reconnect before host migration this.attemptReconnect(hostPeerId, seedData, myId); }); // v8.36: Add error handler for connection failures conn.on('error', (connErr) => { console.error('[JOINER] Connection error:', connErr); this.attemptReconnect(hostPeerId, seedData, myId); }); this.hostConnection = conn; }); this.myPeer.on('error', (err) => { console.error('[JOINER] Peer error:', err); // v8.36: Try reconnect before becoming host if (err.type === 'network' || err.type === 'peer-unavailable') { this.attemptReconnect(hostPeerId, seedData, null); } else { showNotification('Connection error: ' + err.type, 'error'); this.becomeHost(this.currentWorld, seedData); } }); }, // v8.36: Auto-reconnect with exponential backoff (8-Strategy Consensus Round 4) attemptReconnect(hostPeerId, seedData, myId) { // Don't reconnect if we're already the host if (this.isHost) return; // Check if we've exceeded max attempts if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.log(`[RECONNECT] Max attempts (${this.maxReconnectAttempts}) reached. Initiating host migration.`); showNotification('⚠️ Connection lost. Checking for new host...', 'warning'); this.handleHostDisconnect(); return; } const delay = this.reconnectDelays[this.reconnectAttempts] || 4000; this.reconnectAttempts++; console.log(`[RECONNECT] Attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`); showNotification(`🔄 Reconnecting... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`, 'info'); this.reconnectTimer = setTimeout(() => { if (DEBUG_LOGGING) console.log(`[RECONNECT] Attempting to reconnect to ${hostPeerId}`); // Try to reconnect if (this.myPeer && !this.myPeer.destroyed) { const conn = this.myPeer.connect(hostPeerId, { reliable: true }); conn.on('open', () => { if (DEBUG_LOGGING) console.log(`[RECONNECT] Successfully reconnected!`); this.reconnectAttempts = 0; showNotification('✅ Reconnected!', 'success'); conn.send({ type: 'REQUEST_STATE' }); if (myId) { conn.send({ type: 'JOIN_QUEUE', peerId: myId }); } // Replace old connection if (this.hostConnection) { try { this.hostConnection.close(); } catch (e) { // Connection already closed } } this.hostConnection = conn; // Re-attach event handlers conn.on('data', (data) => { this.handleHostMessage(data, seedData); }); conn.on('close', () => { this.attemptReconnect(hostPeerId, seedData, myId); }); conn.on('error', (connErr) => { console.error('[RECONNECT] Connection error:', connErr); this.attemptReconnect(hostPeerId, seedData, myId); }); }); conn.on('error', (connErr) => { console.error(`[RECONNECT] Attempt ${this.reconnectAttempts} failed:`, connErr); // Retry with next exponential backoff this.attemptReconnect(hostPeerId, seedData, myId); }); } else { // Peer destroyed, can't reconnect this.handleHostDisconnect(); } }, delay); }, handleHostMessage(data, seedData) { switch (data.type) { case 'WORLD_STATE': console.log('[JOINER] Received world state'); this.hostQueue = data.hostQueue || []; // v7.1: Record encountered player if (data.hostInfo && window.EncounteredPlayers) { EncounteredPlayers.recordPlayer(this.hostPeerId, data.hostInfo); } break; case 'PARTICIPANT_UPDATE': // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[JOINER] Participants: ${data.count}`); this.hostQueue = data.hostQueue || []; // v7.1: Update world chat online count if (window.WorldChat) WorldChat.updateOnlineCount(data.count); break; case 'NEW_HOST': // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[JOINER] New host: ${data.hostPeerId}`); this.hostPeerId = data.hostPeerId; this.hostQueue = data.hostQueue || []; break; // v7.1: World Chat message relay case 'WORLD_CHAT': if (window.WorldChat) { WorldChat.receiveMessage(data.sender, data.text, data.timestamp); } break; // v7.1: Emote relay case 'PLAYER_EMOTE': if (window.EmoteSystem) { EmoteSystem.receiveEmote(data.peerId || 'host', data.emoteId); } break; // v7.1: Player info update case 'PLAYER_INFO': if (window.EncounteredPlayers && data.playerInfo) { EncounteredPlayers.recordPlayer(data.peerId, data.playerInfo); } break; } }, handleHostDisconnect() { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log('[HOST MIGRATION] Host left - checking if we should become host'); this.hostQueue.shift(); if (this.hostQueue.length > 0 && this.hostQueue[0] === this.myPeer?.id) { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log('[HOST MIGRATION] We are next in queue - BECOMING HOST'); this.promoteToHost(); } else if (this.hostQueue.length > 0) { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[HOST MIGRATION] Reconnecting to new host: ${this.hostQueue[0]}`); showNotification('Host left - reconnecting...', 'warning'); } else { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log('[HOST MIGRATION] No queue - BECOMING HOST'); this.promoteToHost(); } }, promoteToHost() { this.isHost = true; this.hostPeerId = this.myPeer.id; this.myPeer.on('connection', (conn) => { this.handleNewParticipant(conn); }); this.showHostQRModal(this.currentWorld, this.myPeer.id); showNotification('👑 You are now the HOST!', 'warning'); this.broadcastToAll({ type: 'NEW_HOST', hostPeerId: this.myPeer.id, hostQueue: this.hostQueue }); }, handleNewParticipant(conn) { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[HOST] New participant connecting: ${conn.peer}`); this.participants.push({ peerId: conn.peer, connection: conn, joinedAt: Date.now() }); this.hostQueue.push(conn.peer); conn.on('open', () => { conn.send({ type: 'WORLD_STATE', state: this.getCurrentWorldState(), hostQueue: this.hostQueue }); this.broadcastToAll({ type: 'PARTICIPANT_UPDATE', count: this.participants.length + 1, hostQueue: this.hostQueue }); showNotification(`👤 Player joined! (${this.participants.length + 1} in world)`, 'info'); }); conn.on('data', (data) => { if (data.type === 'REQUEST_STATE') { conn.send({ type: 'WORLD_STATE', state: this.getCurrentWorldState(), hostQueue: this.hostQueue }); } // v7.1: Handle World Chat from participants else if (data.type === 'WORLD_CHAT') { // Show locally if (window.WorldChat) { WorldChat.receiveMessage(data.sender, data.text, data.timestamp); } // Relay to all other participants this.broadcastToAll({ type: 'WORLD_CHAT', sender: data.sender, text: data.text, timestamp: data.timestamp }, conn.peer); // Exclude sender } // v7.1: Handle Emote from participants else if (data.type === 'PLAYER_EMOTE') { // Show locally if (window.EmoteSystem) { EmoteSystem.receiveEmote(conn.peer, data.emoteId); } // Relay to all other participants this.broadcastToAll({ type: 'PLAYER_EMOTE', peerId: conn.peer, emoteId: data.emoteId, timestamp: data.timestamp }, conn.peer); } // v7.1: Handle Player Info update else if (data.type === 'PLAYER_INFO') { if (window.EncounteredPlayers && data.playerInfo) { EncounteredPlayers.recordPlayer(conn.peer, data.playerInfo); } } }); conn.on('close', () => { this.handleParticipantLeave(conn.peer); }); }, handleParticipantLeave(peerId) { this.participants = this.participants.filter(p => p.peerId !== peerId); this.hostQueue = this.hostQueue.filter(id => id !== peerId); this.broadcastToAll({ type: 'PARTICIPANT_UPDATE', count: this.participants.length + 1, hostQueue: this.hostQueue }); showNotification(`👤 Player left (${this.participants.length + 1} in world)`, 'info'); }, broadcastToAll(data, excludePeerId = null) { this.participants.forEach(p => { // v7.1: Support excluding sender when relaying messages if (excludePeerId && p.peerId === excludePeerId) return; try { p.connection.send(data); } catch (e) { console.error(`Failed to send to ${p.peerId}:`, e); } }); }, getCurrentWorldState() { return { worldId: this.currentWorld, timestamp: Date.now(), timeOfDay: typeof timeOfDay !== 'undefined' ? timeOfDay : 0.5 }; }, applySeedData(seedData) { console.log('[WORLD] Applying seed data:', seedData.config.name); // v7.3: Store active world config for system access window.ACTIVE_WORLD_CONFIG = seedData; // Normalize biome name to match BIOMES keys (capitalize first letter) let biome = seedData.config.biome || 'Terra'; biome = biome.charAt(0).toUpperCase() + biome.slice(1).toLowerCase(); const validBiomes = ['Terra', 'Desert', 'Ice', 'Alien', 'Volcanic']; if (!validBiomes.includes(biome)) { console.warn(`[WORLD] Unknown biome '${biome}', defaulting to Terra`); biome = 'Terra'; } // v7.3: Process system toggles - WORLDS CAN BE COMPLETELY DIFFERENT const systems = seedData.systems || {}; this.applyWorldSystems(systems); // v7.3: Apply custom visuals BEFORE world generation this.applyWorldVisuals(seedData.config); if (typeof applyFullState === 'function') { applyFullState({ type: 'fullState', worldSeed: seedData.terrain?.seed || seedData.worldId, civilization: { id: Math.floor(Math.random() * 1000), name: seedData.config.name, biome: biome }, world: { timeOfDay: seedData.config.timeOfDay || 0.5, player: { position: seedData.spawn?.position || { x: 0, y: 10, z: 0 } } }, structures: seedData.structures || [], agents: seedData.agents || [] }); } // v9.9: AGGRESSIVE multi-pass cleanup for custom worlds // Run cleanup multiple times to catch async-spawned objects const runCleanup = () => { this.postGenerationCleanup(systems); }; // First pass - immediate setTimeout(() => { runCleanup(); this.spawnWorldObjects(seedData); this.applyWorldUI(seedData.ui || {}); }, 500); // Second pass - catch late spawners setTimeout(runCleanup, 1000); // Third pass - final cleanup setTimeout(runCleanup, 2000); // Fourth pass - absolutely ensure everything is gone if (systems.customOnly) { setTimeout(runCleanup, 3000); setTimeout(runCleanup, 5000); console.log('[WORLD] ☢️ Scheduled 5 cleanup passes for customOnly world'); } document.getElementById('loading').style.display = 'none'; }, // v9.9: COMPREHENSIVE World System Control - EVERYTHING can be customized applyWorldSystems(systems) { console.log('[WORLD] v9.9: Applying COMPREHENSIVE system config:', systems); // v9.9: Complete control over ALL game systems window.WORLD_SYSTEMS = { // Entities mobs: systems.mobs !== false, creeps: systems.creeps !== false, npcs: systems.npcs !== false, agents: systems.agents !== false, merchants: systems.merchants !== false, // Structures ship: systems.ship !== false, buildings: systems.buildings !== false, towers: systems.towers !== false, barracks: systems.barracks !== false, spawnPlatforms: systems.spawnPlatforms !== false, portals: systems.portals !== false, // Natural features trees: systems.trees !== false, rocks: systems.rocks !== false, plants: systems.plants !== false, water: systems.water !== false, // Terrain terrain: systems.terrain !== false, terrainFeatures: systems.terrainFeatures !== false, groundDecor: systems.groundDecor !== false, // Gameplay systems towerDefense: systems.towerDefense !== false, creepWaves: systems.creepWaves !== false, combat: systems.combat !== false, resources: systems.resources !== false, inventory: systems.inventory !== false, building: systems.building !== false, crafting: systems.crafting !== false, experience: systems.experience !== false, quests: systems.quests !== false, lore: systems.lore !== false, // Environment dayNight: systems.dayNight !== false, weather: systems.weather !== false, particles: systems.particles !== false, ambientSounds: systems.ambientSounds !== false, music: systems.music !== false, // Visual effects fog: systems.fog !== false, shadows: systems.shadows !== false, bloom: systems.bloom !== false, // UI elements minimap: systems.minimap !== false, healthBar: systems.healthBar !== false, abilities: systems.abilities !== false, hotbar: systems.hotbar !== false, chat: systems.chat !== false, notifications: systems.notifications !== false, // Special modes customOnly: systems.customOnly === true, // NUCLEAR: Remove everything preserveTerrain: systems.preserveTerrain !== false, // Keep ground mesh preservePlayer: systems.preservePlayer !== false, // Keep player preserveLights: systems.preserveLights === true, // Keep default lights preserveCamera: systems.preserveCamera === true // Keep camera settings }; // Immediately disable spawners if (!window.WORLD_SYSTEMS.mobs && typeof MOB_SPAWNER !== 'undefined') { MOB_SPAWNER.enabled = false; } if (!window.WORLD_SYSTEMS.creepWaves && typeof creepWaveState !== 'undefined') { creepWaveState.enabled = false; // v9.10: Fixed - was .active, should be .enabled creepWaveState.waveNumber = 0; } if (!window.WORLD_SYSTEMS.towerDefense) { if (typeof towerDefenseState !== 'undefined') towerDefenseState.active = false; } console.log('[WORLD] System config applied. CustomOnly:', window.WORLD_SYSTEMS.customOnly); }, // v7.3: Apply custom visual settings applyWorldVisuals(config) { console.log('[WORLD] Applying visual config'); // Custom sky color if (config.skyColor && scene) { scene.background = new THREE.Color(config.skyColor); } // Custom fog if (config.fogColor !== undefined) { if (config.fogColor === false || config.fogColor === 'none') { scene.fog = null; } else { const fogColor = new THREE.Color(config.fogColor); const fogDensity = config.fogDensity || 0.015; const fogNear = config.fogNear || 10; const fogFar = config.fogFar || 150; scene.fog = config.fogType === 'exponential' ? new THREE.FogExp2(fogColor, fogDensity) : new THREE.Fog(fogColor, fogNear, fogFar); } } // Custom ambient light if (config.ambientColor && worldState.ambient) { worldState.ambient.color = new THREE.Color(config.ambientColor); } if (config.ambientIntensity !== undefined && worldState.ambient) { worldState.ambient.intensity = config.ambientIntensity; } // Custom sun settings if (config.sunColor && worldState.sun) { worldState.sun.color = new THREE.Color(config.sunColor); } if (config.sunIntensity !== undefined && worldState.sun) { worldState.sun.intensity = config.sunIntensity; } // Eternal day/night (disable cycle) if (config.timeOfDay !== undefined && config.timeFrozen === true) { window.WORLD_TIME_FROZEN = true; window.WORLD_FROZEN_TIME = config.timeOfDay; } }, // v9.9: NUCLEAR POST-GENERATION CLEANUP - Remove EVERYTHING for custom worlds postGenerationCleanup(systems) { console.log('[WORLD] v9.9: NUCLEAR post-generation cleanup starting...'); const WS = window.WORLD_SYSTEMS; // Helper to safely remove from scene const safeRemove = (obj) => { if (obj && obj.parent) { scene.remove(obj); if (obj.geometry) obj.geometry.dispose(); if (obj.material) { if (Array.isArray(obj.material)) { obj.material.forEach(m => m.dispose()); } else { obj.material.dispose(); } } } }; // ========================================== // NUCLEAR OPTION: customOnly = true // Remove ABSOLUTELY EVERYTHING except terrain and player // ========================================== if (WS.customOnly) { console.log('[WORLD] ☢️ NUCLEAR MODE: Removing ALL default objects'); // Track what to preserve const preserveList = []; // Preserve terrain mesh if enabled if (WS.preserveTerrain && worldState.ground) { preserveList.push(worldState.ground); } // Preserve player if (WS.preservePlayer && worldState.player) { preserveList.push(worldState.player); } // Preserve camera preserveList.push(camera); // Preserve essential lights (ambient and directional only) if (WS.preserveLights) { if (worldState.ambient) preserveList.push(worldState.ambient); if (worldState.sun) preserveList.push(worldState.sun); } // Remove EVERYTHING else from scene const toRemove = []; scene.traverse((child) => { if (!preserveList.includes(child) && child !== scene) { // Check if it's not a light we want to keep const isEssentialLight = (child instanceof THREE.AmbientLight || child instanceof THREE.DirectionalLight) && WS.preserveLights; // v9.9.1: PRESERVE custom objects spawned from seed data! const isCustomObject = child.userData && child.userData.customObject === true; if (!isEssentialLight && !isCustomObject) { toRemove.push(child); } } }); // Count custom objects that will be preserved let customObjCount = 0; scene.traverse((child) => { if (child.userData?.customObject) customObjCount++; }); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[WORLD] ☢️ Removing ${toRemove.length} objects, preserving ${customObjCount} custom objects`); toRemove.forEach(obj => safeRemove(obj)); // Clear all game state arrays if (worldState.interactables) { worldState.interactables = []; } if (worldState.mobs) { worldState.mobs = []; } if (typeof creeps !== 'undefined' && Array.isArray(creeps)) { creeps.length = 0; } if (typeof structures !== 'undefined' && Array.isArray(structures)) { structures.length = 0; } if (typeof agentFleet !== 'undefined' && Array.isArray(agentFleet)) { agentFleet.forEach(a => safeRemove(a.mesh)); agentFleet.length = 0; } // Remove ship if (typeof SHIP_STATE !== 'undefined' && SHIP_STATE.mesh) { safeRemove(SHIP_STATE.mesh); SHIP_STATE.mesh = null; SHIP_STATE.landed = true; SHIP_STATE.hidden = true; } // Remove spawn platforms if (typeof spawnPlatforms !== 'undefined' && Array.isArray(spawnPlatforms)) { spawnPlatforms.forEach(sp => safeRemove(sp.mesh)); spawnPlatforms.length = 0; } // Disable all spawning systems if (typeof MOB_SPAWNER !== 'undefined') MOB_SPAWNER.enabled = false; if (typeof waveState !== 'undefined') waveState.enabled = false; // v9.10: Also disable wave momentum system if (typeof creepWaveState !== 'undefined') { creepWaveState.enabled = false; // v9.10: Fixed - was .active, should be .enabled creepWaveState.waveNumber = 0; } console.log('[WORLD] ☢️ NUCLEAR cleanup complete - world is now bare'); return; // Skip individual checks } // ========================================== // GRANULAR CONTROL: Individual system toggles // ========================================== // Remove mobs if (!WS.mobs && worldState.mobs) { console.log('[WORLD] Removing mobs'); worldState.mobs.forEach(mob => safeRemove(mob)); worldState.mobs = []; } // Remove creeps if (!WS.creeps && typeof creeps !== 'undefined') { console.log('[WORLD] Removing creeps'); creeps.forEach(c => safeRemove(c.mesh || c)); creeps.length = 0; } // Remove ship if (!WS.ship && typeof SHIP_STATE !== 'undefined' && SHIP_STATE.mesh) { console.log('[WORLD] Removing ship'); safeRemove(SHIP_STATE.mesh); SHIP_STATE.mesh = null; SHIP_STATE.landed = true; SHIP_STATE.hidden = true; } // Remove towers if (!WS.towers) { console.log('[WORLD] Removing towers'); scene.children.filter(c => c.userData?.isTower).forEach(safeRemove); } // Remove barracks if (!WS.barracks) { console.log('[WORLD] Removing barracks'); scene.children.filter(c => c.userData?.isBarracks).forEach(safeRemove); } // Remove spawn platforms if (!WS.spawnPlatforms) { console.log('[WORLD] Removing spawn platforms'); scene.children.filter(c => c.userData?.isSpawnPlatform).forEach(safeRemove); if (typeof spawnPlatforms !== 'undefined') { spawnPlatforms.forEach(sp => safeRemove(sp.mesh)); spawnPlatforms.length = 0; } } // Remove buildings if (!WS.buildings) { console.log('[WORLD] Removing buildings'); scene.children.filter(c => c.userData?.isBuilding || c.userData?.type === 'building' || c.userData?.type === 'structure' ).forEach(safeRemove); } // Remove trees if (!WS.trees && worldState.interactables) { console.log('[WORLD] Removing trees'); worldState.interactables = worldState.interactables.filter(obj => { if (obj.userData?.type === 'tree' || obj.userData?.isTree) { safeRemove(obj); return false; } return true; }); } // Remove rocks if (!WS.rocks && worldState.interactables) { console.log('[WORLD] Removing rocks'); worldState.interactables = worldState.interactables.filter(obj => { if (obj.userData?.type === 'rock' || obj.userData?.isRock) { safeRemove(obj); return false; } return true; }); } // Remove plants if (!WS.plants && worldState.interactables) { console.log('[WORLD] Removing plants'); worldState.interactables = worldState.interactables.filter(obj => { if (obj.userData?.type === 'plant' || obj.userData?.isPlant) { safeRemove(obj); return false; } return true; }); } // Remove ground decorations if (!WS.groundDecor) { console.log('[WORLD] Removing ground decorations'); scene.children.filter(c => c.userData?.isGroundDecor || c.userData?.type === 'grass' || c.userData?.type === 'flower' || c.userData?.type === 'decor' ).forEach(safeRemove); } // Remove agents if (!WS.agents && typeof agentFleet !== 'undefined') { console.log('[WORLD] Removing agents'); agentFleet.forEach(a => safeRemove(a.mesh)); agentFleet.length = 0; } // Remove NPCs if (!WS.npcs) { console.log('[WORLD] Removing NPCs'); scene.children.filter(c => c.userData?.isNPC || c.userData?.type === 'npc').forEach(safeRemove); } // Remove portals if (!WS.portals) { console.log('[WORLD] Removing portals'); scene.children.filter(c => c.userData?.isPortal || c.userData?.type === 'portal').forEach(safeRemove); } // Remove particles if (!WS.particles) { console.log('[WORLD] Removing particle systems'); scene.children.filter(c => c instanceof THREE.Points || c.userData?.isParticleSystem).forEach(safeRemove); } console.log('[WORLD] Post-generation cleanup complete'); }, // v7.3: Spawn custom world objects from seed spawnWorldObjects(seedData) { if (!seedData.customObjects) return; console.log('[WORLD] Spawning custom objects:', seedData.customObjects.length); seedData.customObjects.forEach(obj => { this.spawnCustomObject(obj); }); }, // v7.3: Spawn a single custom object (v9.9: Fixed scale handling) spawnCustomObject(config) { let mesh; const pos = config.position || { x: 0, y: 0, z: 0 }; const scale = config.scale || { x: 1, y: 1, z: 1 }; const color = config.color ? new THREE.Color(config.color) : new THREE.Color(0xffffff); const emissive = config.emissive ? new THREE.Color(config.emissive) : new THREE.Color(0x000000); const emissiveIntensity = config.emissiveIntensity || 0; const opacity = config.opacity !== undefined ? config.opacity : 1.0; const transparent = config.transparent === true || opacity < 1.0; // v9.9: Build material with full options const buildMaterial = () => new THREE.MeshStandardMaterial({ color, emissive, emissiveIntensity, opacity, transparent, metalness: config.metalness || 0.1, roughness: config.roughness !== undefined ? config.roughness : 0.5 }); // v9.9: Handle scale as object {x,y,z} or number const scaleX = typeof scale === 'object' ? (scale.x || 1) : scale; const scaleY = typeof scale === 'object' ? (scale.y || 1) : scale; const scaleZ = typeof scale === 'object' ? (scale.z || 1) : scale; switch (config.type) { case 'sphere': mesh = new THREE.Mesh( new THREE.SphereGeometry(1, 32, 32), buildMaterial() ); break; case 'box': case 'cube': mesh = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), buildMaterial() ); break; case 'cylinder': mesh = new THREE.Mesh( new THREE.CylinderGeometry(0.5, 0.5, 1, 32), buildMaterial() ); break; case 'cone': mesh = new THREE.Mesh( new THREE.ConeGeometry(0.5, 1, 32), buildMaterial() ); break; case 'torus': case 'ring': mesh = new THREE.Mesh( new THREE.TorusGeometry(1, 0.1, 16, 100), buildMaterial() ); break; case 'plane': mesh = new THREE.Mesh( new THREE.PlaneGeometry(1, 1), buildMaterial() ); break; case 'light-point': mesh = new THREE.PointLight(color, config.intensity || 1, config.distance || 50); break; case 'light-spot': mesh = new THREE.SpotLight(color, config.intensity || 1); mesh.angle = config.angle || Math.PI / 6; break; case 'light-ambient': mesh = new THREE.AmbientLight(color, config.intensity || 0.5); break; case 'particle-emitter': mesh = this.createParticleEmitter(config); break; case 'floating-text': mesh = this.createFloatingText(config); break; case 'portal': mesh = this.createPortal(config); break; default: console.warn('[WORLD] Unknown custom object type:', config.type); return; } if (mesh) { mesh.position.set(pos.x, pos.y, pos.z); if (config.rotation) { mesh.rotation.set( (config.rotation.x || 0) * Math.PI / 180, (config.rotation.y || 0) * Math.PI / 180, (config.rotation.z || 0) * Math.PI / 180 ); } // v9.9: Apply scale as vector, not scalar mesh.scale.set(scaleX, scaleY, scaleZ); mesh.userData.customObject = true; mesh.userData.config = config; mesh.userData.description = config.description || ''; // v9.9: Cast shadows for solid objects if (mesh.isMesh) { mesh.castShadow = config.castShadow !== false; mesh.receiveShadow = config.receiveShadow !== false; } scene.add(mesh); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[WORLD] Spawned custom ${config.type} at (${pos.x}, ${pos.y}, ${pos.z}) scale (${scaleX}, ${scaleY}, ${scaleZ})`); // Animate if specified if (config.animate) { this.animateCustomObject(mesh, config.animate); } } }, // v7.3: Create particle emitter createParticleEmitter(config) { const group = new THREE.Group(); const particleCount = config.count || 50; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(particleCount * 3); for (let i = 0; i < particleCount * 3; i += 3) { positions[i] = (Math.random() - 0.5) * (config.spread || 10); positions[i + 1] = Math.random() * (config.height || 20); positions[i + 2] = (Math.random() - 0.5) * (config.spread || 10); } geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const material = new THREE.PointsMaterial({ color: config.color || 0xffffff, size: config.particleSize || 0.5, transparent: true, opacity: config.opacity || 0.8 }); group.add(new THREE.Points(geometry, material)); group.userData.isParticleEmitter = true; return group; }, // v7.3: Create floating text (3D sprite) createFloatingText(config) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = 512; canvas.height = 128; ctx.fillStyle = config.backgroundColor || 'transparent'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.font = `${config.fontSize || 48}px ${config.fontFamily || 'Arial'}`; ctx.fillStyle = config.color || '#ffffff'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(config.text || '', canvas.width / 2, canvas.height / 2); const texture = new THREE.CanvasTexture(canvas); const material = new THREE.SpriteMaterial({ map: texture, transparent: true }); const sprite = new THREE.Sprite(material); sprite.scale.set(config.width || 10, config.height || 2.5, 1); return sprite; }, // v7.3: Create portal object createPortal(config) { const group = new THREE.Group(); // Portal ring const ring = new THREE.Mesh( new THREE.TorusGeometry(config.radius || 3, 0.3, 16, 100), new THREE.MeshStandardMaterial({ color: config.ringColor || 0x00ffff, emissive: config.ringColor || 0x00ffff, emissiveIntensity: 0.5 }) ); group.add(ring); // Portal surface const surface = new THREE.Mesh( new THREE.CircleGeometry(config.radius || 3, 32), new THREE.MeshBasicMaterial({ color: config.surfaceColor || 0x000033, transparent: true, opacity: 0.8, side: THREE.DoubleSide }) ); group.add(surface); // Light const light = new THREE.PointLight(config.ringColor || 0x00ffff, 2, 20); group.add(light); group.userData.isPortal = true; group.userData.destination = config.destination; return group; }, // v7.3: Animate custom objects animateCustomObject(mesh, animConfig) { const animate = () => { if (!mesh.parent) return; // Stop if removed if (animConfig.rotate) { mesh.rotation.x += (animConfig.rotate.x || 0) * 0.01; mesh.rotation.y += (animConfig.rotate.y || 0) * 0.01; mesh.rotation.z += (animConfig.rotate.z || 0) * 0.01; } if (animConfig.float) { mesh.position.y += Math.sin(Date.now() * 0.001 * (animConfig.float.speed || 1)) * 0.01 * (animConfig.float.amplitude || 1); } // v12.26: Check emissive support to prevent MeshBasicMaterial warnings if (animConfig.pulse && mesh.material?.emissiveIntensity !== undefined) { const pulse = 0.5 + Math.sin(Date.now() * 0.003 * (animConfig.pulse.speed || 1)) * 0.5; mesh.material.emissiveIntensity = pulse * (animConfig.pulse.intensity || 1); } requestAnimationFrame(animate); }; animate(); }, // v7.3: Apply custom UI settings applyWorldUI(uiConfig) { // Hide/show UI elements based on world config const hideElements = (selector, hide) => { const el = document.querySelector(selector); if (el) el.style.display = hide ? 'none' : ''; }; if (uiConfig.hideHealthBar) hideElements('#player-health-bar', true); if (uiConfig.hideMinimap) hideElements('#minimap', true); if (uiConfig.hideInventory) hideElements('#inventory', true); if (uiConfig.hideAIBehavior) hideElements('#ai-behavior-container', true); if (uiConfig.hideAbilities) hideElements('#abilities-bar', true); if (uiConfig.hideCompass) hideElements('#compass', true); // Custom HUD overlay if (uiConfig.customHUD) { this.createCustomHUD(uiConfig.customHUD); } // World-specific title if (uiConfig.worldTitle) { this.showWorldTitle(uiConfig.worldTitle, uiConfig.worldSubtitle); } }, // v7.3: Show world title on entry showWorldTitle(title, subtitle) { const overlay = document.createElement('div'); overlay.id = 'world-title-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; z-index: 9999; animation: fadeOut 3s ease-in-out 2s forwards; `; overlay.innerHTML = `
${title}
${subtitle ? `
${subtitle}
` : ''} `; document.body.appendChild(overlay); setTimeout(() => overlay.remove(), 5000); }, createCustomHUD(hudConfig) { // Allow worlds to inject custom HUD elements if (hudConfig.html) { const hud = document.createElement('div'); hud.id = 'custom-world-hud'; hud.innerHTML = hudConfig.html; hud.style.cssText = hudConfig.style || 'position: fixed; top: 10px; left: 10px; z-index: 1000;'; document.body.appendChild(hud); } }, showHostQRModal(worldId, hostPeerId) { const existingModal = document.getElementById('host-qr-modal'); if (existingModal) existingModal.remove(); const baseUrl = window.location.origin + window.location.pathname; const joinUrl = `${baseUrl}?world=${worldId}&host=${hostPeerId}`; const modal = document.createElement('div'); modal.id = 'host-qr-modal'; modal.innerHTML = `

🌍 YOU ARE HOSTING

${worldId}
● LIVE ${this.participants.length + 1} in world
Scan QR code or share link to invite others.
Your world is LIVE as long as you stay.
`; document.body.appendChild(modal); setTimeout(() => { loadQRiousLibrary().then(() => { const canvas = document.getElementById('host-qr-canvas'); if (canvas) { new window.QRious({ element: canvas, value: joinUrl, size: 200, backgroundAlpha: 0, foreground: '#0ff', level: 'M' }); } }); }, 100); } }; // v7.0: World Store - Browse and join public worlds const WorldStore = { async open() { const registry = await PublicWorldManager.loadRegistry(); if (!registry) { showNotification('Failed to load world registry', 'error'); return; } const modal = document.createElement('div'); modal.id = 'world-store'; modal.innerHTML = `

🌌 PUBLIC WORLDS

${registry.worlds.map(w => this.renderWorldCard(w)).join('')}
`; document.body.appendChild(modal); }, renderWorldCard(world) { return `
${world.featured ? '★ FEATURED' : ''}
${this.getWorldIcon(world.category)}
${world.name}
by ${world.author}
${world.description}
👥 ${world.totalVisitors || 0} ⚡ ${world.temporalContributions || 0}
`; }, getWorldIcon(category) { const icons = { exploration: '🌋', social: '👥', creative: '🎨', challenge: '⚔️', story: '📖' }; return icons[category] || '🌍'; }, async joinWorld(worldId) { this.close(); await PublicWorldManager.joinWorld(worldId); }, close() { document.getElementById('world-store')?.remove(); }, showTab(tab) { // Future: filter worlds by tab console.log('Showing tab:', tab); } }; // v7.22: Expose to window for inline onclick handler window.WorldStore = WorldStore; // ============================================ // v9.9: PUBLIC GALAXY SYSTEM // Permanent, repo-based galaxy that shows all public worlds // organized by star systems. Shared state across all players. // ============================================ const PublicGalaxy = { GALAXY_URL: 'https://raw.githubusercontent.com/kody-w/localFirstTools/main/data/public-worlds/galaxy.json', REGISTRY_URL: 'https://raw.githubusercontent.com/kody-w/localFirstTools/main/data/public-worlds/registry.json', galaxyData: null, registryData: null, isOpen: false, galaxyScene: null, galaxyCamera: null, galaxyRenderer: null, galaxyControls: null, animationFrameId: null, starMeshes: [], connectionLines: [], selectedSystem: null, hoveredSystem: null, raycaster: null, mouse: null, async loadGalaxyData() { try { const [galaxyResponse, registryResponse] = await Promise.all([ fetch(this.GALAXY_URL + '?t=' + Date.now()), fetch(this.REGISTRY_URL + '?t=' + Date.now()) ]); this.galaxyData = await galaxyResponse.json(); this.registryData = await registryResponse.json(); console.log('[PUBLIC GALAXY] Loaded galaxy with', this.galaxyData.starSystems.length, 'star systems'); return true; } catch (error) { console.error('[PUBLIC GALAXY] Failed to load:', error); return false; } }, async open() { if (this.isOpen) return; const loadingOverlay = document.createElement('div'); loadingOverlay.id = 'galaxy-loading'; loadingOverlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.95);display:flex;align-items:center;justify-content:center;z-index:10001;'; loadingOverlay.innerHTML = '
🌌
Loading the Omniverse...
'; document.body.appendChild(loadingOverlay); const loaded = await this.loadGalaxyData(); if (!loaded) { loadingOverlay.remove(); showNotification('Failed to load Public Galaxy', 'error'); return; } this.isOpen = true; loadingOverlay.remove(); this.createGalaxyView(); }, createGalaxyView() { const container = document.createElement('div'); container.id = 'public-galaxy-view'; container.style.cssText = 'position:fixed;inset:0;z-index:10000;background:#050510;'; const canvas = document.createElement('canvas'); canvas.id = 'galaxy-canvas'; container.appendChild(canvas); const uiOverlay = document.createElement('div'); uiOverlay.id = 'galaxy-ui'; uiOverlay.innerHTML = this.createUIHTML(); container.appendChild(uiOverlay); document.body.appendChild(container); this.initThreeJS(canvas); this.createStarfield(); this.createStarSystems(); this.createConnections(); this.setupInteraction(); this.startAnimation(); document.getElementById('galaxy-close-btn').addEventListener('click', () => this.close()); this.updateInfoPanel(null); }, createUIHTML() { return `

${this.galaxyData.name}

${this.galaxyData.description}
Click a star system to explore
SYSTEM TYPES
${Object.entries(this.galaxyData.systemTypes || {}).slice(0,6).map(([key, val]) => `
${key.charAt(0).toUpperCase() + key.slice(1)}
`).join('')}
Star Systems: ${this.galaxyData.starSystems.length}
Total Worlds: ${this.registryData.worlds.length}
Version: ${this.galaxyData.version}
Drag to rotate | Scroll to zoom | Click stars to explore
`; }, initThreeJS(canvas) { const width = window.innerWidth; const height = window.innerHeight; this.galaxyScene = new THREE.Scene(); this.galaxyCamera = new THREE.PerspectiveCamera(60, width / height, 1, 3000); this.galaxyCamera.position.set(0, 200, 500); this.galaxyRenderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); this.galaxyRenderer.setSize(width, height); this.galaxyRenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); if (typeof THREE.OrbitControls !== 'undefined') { this.galaxyControls = new THREE.OrbitControls(this.galaxyCamera, canvas); this.galaxyControls.enableDamping = true; this.galaxyControls.dampingFactor = 0.05; this.galaxyControls.maxDistance = 1000; this.galaxyControls.minDistance = 100; } this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); const ambient = new THREE.AmbientLight(0x222244, 0.5); this.galaxyScene.add(ambient); window.addEventListener('resize', () => { if (!this.isOpen) return; this.galaxyCamera.aspect = window.innerWidth / window.innerHeight; this.galaxyCamera.updateProjectionMatrix(); this.galaxyRenderer.setSize(window.innerWidth, window.innerHeight); }); }, createStarfield() { const starGeometry = new THREE.BufferGeometry(); const starCount = this.galaxyData.visualConfig?.starfieldDensity || 500; const positions = new Float32Array(starCount * 3); for (let i = 0; i < starCount * 3; i += 3) { positions[i] = (Math.random() - 0.5) * 2000; positions[i + 1] = (Math.random() - 0.5) * 2000; positions[i + 2] = (Math.random() - 0.5) * 2000; } starGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 1, sizeAttenuation: true }); const stars = new THREE.Points(starGeometry, starMaterial); this.galaxyScene.add(stars); }, createStarSystems() { this.starMeshes = []; const systems = this.galaxyData.starSystems; systems.forEach(system => { const color = new THREE.Color(system.color); const size = (system.size || 2) * 8; const coreGeo = new THREE.SphereGeometry(size, 32, 32); const coreMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.9 }); const core = new THREE.Mesh(coreGeo, coreMat); core.position.set(system.position.x, system.position.y, system.position.z); core.userData = { systemId: system.systemId, systemData: system }; const glowGeo = new THREE.SphereGeometry(size * 1.5, 32, 32); const glowMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.3 }); const glow = new THREE.Mesh(glowGeo, glowMat); core.add(glow); const ringGeo = new THREE.TorusGeometry(size * 2, 1, 8, 32); const ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.5 }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.rotation.x = Math.PI / 2; core.add(ring); this.galaxyScene.add(core); this.starMeshes.push(core); }); }, createConnections() { const connections = this.galaxyData.connections || []; const systemMap = {}; this.galaxyData.starSystems.forEach(s => systemMap[s.systemId] = s); connections.forEach(conn => { const fromSys = systemMap[conn.from]; const toSys = systemMap[conn.to]; if (!fromSys || !toSys) return; const points = [ new THREE.Vector3(fromSys.position.x, fromSys.position.y, fromSys.position.z), new THREE.Vector3(toSys.position.x, toSys.position.y, toSys.position.z) ]; const lineGeo = new THREE.BufferGeometry().setFromPoints(points); const lineMat = new THREE.LineBasicMaterial({ color: conn.type === 'trade-route' ? 0x0088ff : conn.type === 'ascension-path' ? 0xffdd00 : 0x00ff88, transparent: true, opacity: 0.2 }); const line = new THREE.Line(lineGeo, lineMat); this.galaxyScene.add(line); this.connectionLines.push(line); }); }, setupInteraction() { const canvas = document.getElementById('galaxy-canvas'); canvas.addEventListener('mousemove', (e) => { this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1; this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; }); canvas.addEventListener('click', () => { this.raycaster.setFromCamera(this.mouse, this.galaxyCamera); const intersects = this.raycaster.intersectObjects(this.starMeshes); if (intersects.length > 0) { const system = intersects[0].object.userData.systemData; this.selectSystem(system); } }); }, selectSystem(system) { this.selectedSystem = system; this.updateInfoPanel(system); if (this.galaxyControls) { const target = new THREE.Vector3(system.position.x, system.position.y, system.position.z); this.galaxyControls.target.lerp(target, 0.5); } }, updateInfoPanel(system) { const panel = document.getElementById('galaxy-panel-content'); if (!panel) return; if (!system) { panel.innerHTML = `
Click a star system to explore
`; // v7.78: contrast fix return; } const worlds = system.worlds.map(wid => this.registryData.worlds.find(w => w.id === wid)).filter(Boolean); panel.innerHTML = `

${system.name}

${system.type}

${system.description}

${worlds.length} world${worlds.length !== 1 ? 's' : ''} in this system
${worlds.map(world => `
${world.name}
${world.description?.slice(0, 80)}${world.description?.length > 80 ? '...' : ''}
${(world.tags || []).slice(0, 3).map(t => `${t}`).join('')}
`).join('')}
`; }, joinWorld(worldId) { this.close(); const baseUrl = window.location.origin + window.location.pathname; window.location.href = `${baseUrl}?world=${encodeURIComponent(worldId)}`; }, startAnimation() { const animate = () => { if (!this.isOpen) return; this.animationFrameId = requestAnimationFrame(animate); // v8.16: forEach-to-for optimization (animation loop hot path) const starMeshes = this.starMeshes; for (let si = 0, slen = starMeshes.length; si < slen; si++) { const star = starMeshes[si]; if (star.children[1]) star.children[1].rotation.z += 0.005; } if (this.galaxyControls) this.galaxyControls.update(); this.raycaster.setFromCamera(this.mouse, this.galaxyCamera); const intersects = this.raycaster.intersectObjects(starMeshes); // v8.16: forEach-to-for optimization (animation loop hot path) for (let si2 = 0, slen2 = starMeshes.length; si2 < slen2; si2++) { starMeshes[si2].scale.setScalar(1); } if (intersects.length > 0) { intersects[0].object.scale.setScalar(1.2); document.getElementById('galaxy-canvas').style.cursor = 'pointer'; } else { document.getElementById('galaxy-canvas').style.cursor = 'grab'; } this.galaxyRenderer.render(this.galaxyScene, this.galaxyCamera); }; animate(); }, close() { this.isOpen = false; if (this.animationFrameId) cancelAnimationFrame(this.animationFrameId); if (this.galaxyRenderer) this.galaxyRenderer.dispose(); // v8.16: forEach-to-for optimization (cleanup path) const meshes = this.starMeshes; for (let mi = 0, mlen = meshes.length; mi < mlen; mi++) { const mesh = meshes[mi]; mesh.geometry.dispose(); mesh.material.dispose(); } this.starMeshes = []; this.connectionLines = []; document.getElementById('public-galaxy-view')?.remove(); } }; window.PublicGalaxy = PublicGalaxy; // ============================================ // v7.0: CONNECTION HEARTBEAT SYSTEM // P2P connection resilience with auto-reconnect // Consensus feature from 8-Strategy Analysis (9/10 impact) // ============================================ const ConnectionHeartbeat = { HEARTBEAT_INTERVAL: 2000, // Send ping every 2 seconds TIMEOUT_THRESHOLD: 6000, // Consider dead after 6 seconds MAX_RECONNECT_ATTEMPTS: 3, RECONNECT_DELAYS: [1000, 2000, 4000], // Exponential backoff connections: new Map(), // peerId -> { lastPing, latency, status, reconnectAttempts } heartbeatTimer: null, isRunning: false, start() { if (this.isRunning) return; this.isRunning = true; // v7.78: Use TimerRegistry for centralized timer management TimerRegistry.setInterval('peer-heartbeat', () => this.tick(), this.HEARTBEAT_INTERVAL); console.log('[HEARTBEAT] System started'); }, stop() { // v7.78: Use TimerRegistry for centralized timer management TimerRegistry.clearInterval('peer-heartbeat'); this.isRunning = false; this.connections.clear(); console.log('[HEARTBEAT] System stopped'); }, // Register a connection to monitor register(peerId, connection) { this.connections.set(peerId, { connection, lastPing: Date.now(), lastPong: Date.now(), latency: 0, status: 'connected', reconnectAttempts: 0 }); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[HEARTBEAT] Registered peer: ${peerId}`); }, // Unregister a connection unregister(peerId) { this.connections.delete(peerId); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[HEARTBEAT] Unregistered peer: ${peerId}`); }, // Main heartbeat tick tick() { const now = Date.now(); this.connections.forEach((data, peerId) => { // Check for timeout if (now - data.lastPong > this.TIMEOUT_THRESHOLD) { if (data.status === 'connected') { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[HEARTBEAT] Connection timeout: ${peerId}`); data.status = 'disconnected'; this.handleDisconnect(peerId, data); } return; } // Send ping if (data.connection && data.connection.open) { try { data.connection.send({ type: 'HEARTBEAT_PING', timestamp: now }); data.lastPing = now; } catch (e) { console.warn(`[HEARTBEAT] Failed to ping ${peerId}:`, e); } } }); // Update UI this.updateUI(); }, // Handle received pong receivePong(peerId, timestamp) { const data = this.connections.get(peerId); if (data) { data.lastPong = Date.now(); data.latency = Date.now() - timestamp; data.status = 'connected'; data.reconnectAttempts = 0; } }, // Handle received ping (respond with pong) receivePing(connection, timestamp) { try { connection.send({ type: 'HEARTBEAT_PONG', timestamp }); } catch (e) { console.warn('[HEARTBEAT] Failed to send pong:', e); } }, // Handle disconnection with reconnect attempts handleDisconnect(peerId, data) { if (data.reconnectAttempts >= this.MAX_RECONNECT_ATTEMPTS) { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[HEARTBEAT] Max reconnect attempts reached for ${peerId}`); showNotification(`Lost connection to ${peerId.substring(0, 8)}...`, 'error'); this.unregister(peerId); // Trigger host migration if needed if (PublicWorldManager.hostPeerId === peerId) { PublicWorldManager.handleHostDisconnect(); } return; } const delay = this.RECONNECT_DELAYS[data.reconnectAttempts] || 4000; data.reconnectAttempts++; data.status = 'reconnecting'; // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[HEARTBEAT] Reconnect attempt ${data.reconnectAttempts} for ${peerId} in ${delay}ms`); showNotification(`Connection unstable... retrying (${data.reconnectAttempts}/${this.MAX_RECONNECT_ATTEMPTS})`, 'warning'); setTimeout(() => { this.attemptReconnect(peerId, data); }, delay); }, attemptReconnect(peerId, data) { // For now, just check if connection came back if (data.connection && data.connection.open) { data.lastPong = Date.now(); data.status = 'connected'; data.reconnectAttempts = 0; showNotification('Connection restored!', 'success'); } else { // Still disconnected, try again this.handleDisconnect(peerId, data); } }, // Update connection status UI updateUI() { const statusEl = document.getElementById('connection-status'); if (!statusEl) return; let worstStatus = 'good'; let totalLatency = 0; let count = 0; this.connections.forEach((data) => { if (data.status === 'disconnected' || data.status === 'reconnecting') { worstStatus = 'bad'; } else if (data.latency > 200 && worstStatus !== 'bad') { worstStatus = 'medium'; } totalLatency += data.latency; count++; }); const avgLatency = count > 0 ? Math.round(totalLatency / count) : 0; const colors = { good: '#0f0', medium: '#ff0', bad: '#f00' }; statusEl.style.color = colors[worstStatus]; statusEl.innerHTML = count > 0 ? `${avgLatency}ms` : ''; }, // Get connection quality for a peer (0-1) getQuality(peerId) { const data = this.connections.get(peerId); if (!data || data.status !== 'connected') return 0; // Quality based on latency: <50ms = 1.0, >500ms = 0.1 return Math.max(0.1, 1 - (data.latency / 500)); } }; // Expose globally window.ConnectionHeartbeat = ConnectionHeartbeat; // v5.5: Autonomous Exploration System let autoExplore = { enabled: true, // Start in auto mode currentTarget: null, lastTargetTime: 0, targetCooldown: 3000, // Time between target switches idleTime: 0, state: 'exploring', // exploring, gathering, combat, idle combatTarget: null, // v7.88: Pre-allocated exploration target vector _explorationTarget: null }; function toggleAutoExplore() { autoExplore.enabled = !autoExplore.enabled; autoExplore.currentTarget = null; updateAutoExploreUI(); showNotification(autoExplore.enabled ? 'AUTOPILOT: Exploring automatically' : 'MANUAL: You have control', 'info'); } function updateAutoExploreUI() { const btn = document.getElementById('auto-explore-btn'); const indicator = document.getElementById('auto-explore-indicator'); if (btn) { btn.textContent = autoExplore.enabled ? 'Take Manual Control' : 'Enable Autopilot'; btn.style.background = autoExplore.enabled ? '#00ff88' : '#ff8844'; } if (indicator) { indicator.textContent = autoExplore.enabled ? '🤖 AUTOPILOT' : '🎮 MANUAL'; indicator.style.color = autoExplore.enabled ? '#00ff88' : '#ff8844'; } } // v7.73: Pre-computed squared thresholds for distanceToSquared() optimization const AUTO_EXPLORE_THRESHOLDS = { combat: 25 * 25, // 625 - combat detection range resource: 50 * 50, // 2500 - resource detection range interactionSq: null, // Set from CONFIG at runtime minExplore: 10 * 10, // 100 - min exploration distance maxExplore: 100 * 100 // 10000 - max exploration distance }; function runAutoExplore(dt) { if (!autoExplore.enabled || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); // v7.73: Cache interaction range squared if (AUTO_EXPLORE_THRESHOLDS.interactionSq === null) { AUTO_EXPLORE_THRESHOLDS.interactionSq = CONFIG.INTERACTION_RANGE * CONFIG.INTERACTION_RANGE; } const interactRangeSq = AUTO_EXPLORE_THRESHOLDS.interactionSq; // Priority 1: Combat - attack nearby enemies // v7.73: Use distanceToSquared() for performance // v8.02: forEach to for loop conversion for hot path const exploreMobs = worldState.mobs; const exploreMobsLen = exploreMobs.length; if (exploreMobsLen > 0) { let nearestMob = null; let nearestDistSq = Infinity; for (let i = 0; i < exploreMobsLen; i++) { const mob = exploreMobs[i]; if (!mob.parent) continue; const distSq = player.position.distanceToSquared(mob.position); if (distSq < AUTO_EXPLORE_THRESHOLDS.combat && distSq < nearestDistSq) { nearestDistSq = distSq; nearestMob = mob; } } if (nearestMob) { autoExplore.state = 'combat'; autoExplore.combatTarget = nearestMob; // Move toward mob if not in range if (nearestDistSq > interactRangeSq) { // v7.86: Use setWorldTarget instead of clone() setWorldTarget(nearestMob.position); worldState.interactTarget = nearestMob; } else { // Attack! worldState.target = null; if (now - worldState.lastActionTime > CONFIG.INTERACTION_COOLDOWN) { performAction(nearestMob); worldState.lastActionTime = now; } } return true; } } // Priority 2: Gather resources - only target actual harvestable resources // v7.73: Use distanceToSquared() for performance let bestResource = null; let bestResourceDistSq = Infinity; // v8.02: forEach to for loop conversion for hot path // Check interactables for actual resources (trees, rocks, ores) const interactables = worldState.interactables; const interactablesLen = interactables.length; for (let i = 0; i < interactablesLen; i++) { const obj = interactables[i]; if (!obj.parent) continue; const name = obj.userData.name || ''; // Only target actual resources, not decorations const isResource = name.includes('Tree') || name.includes('Rock') || name.includes('Ore') || name.includes('Crystal') || name.includes('Bush') || name.includes('Plant') || name.includes('Mushroom') || name.includes('Herb'); if (!isResource) continue; const distSq = player.position.distanceToSquared(obj.position); if (distSq < AUTO_EXPLORE_THRESHOLDS.resource && distSq < bestResourceDistSq) { bestResourceDistSq = distSq; bestResource = obj; } } // Also check fishing spots if (!bestResource && worldState.fishingSpots) { // v8.02: forEach to for loop conversion const fishingSpots = worldState.fishingSpots; const fishingSpotsLen = fishingSpots.length; for (let i = 0; i < fishingSpotsLen; i++) { const spot = fishingSpots[i]; if (!spot.parent) continue; const distSq = player.position.distanceToSquared(spot.position); if (distSq < AUTO_EXPLORE_THRESHOLDS.resource && distSq < bestResourceDistSq) { bestResourceDistSq = distSq; bestResource = spot; } } } if (bestResource) { autoExplore.state = 'gathering'; autoExplore.currentTarget = null; // Clear random exploration target if (bestResourceDistSq > interactRangeSq) { // v7.86: Use setWorldTarget instead of clone() setWorldTarget(bestResource.position); worldState.interactTarget = bestResource; } else { worldState.target = null; if (now - worldState.lastActionTime > CONFIG.INTERACTION_COOLDOWN) { performAction(bestResource); worldState.lastActionTime = now; } } return true; } // Priority 3: Explore - find new resources autoExplore.state = 'exploring'; // v7.73: Pick a new random target periodically or if stuck - use distanceToSquared() const currentDistSq = autoExplore.currentTarget ? player.position.distanceToSquared(autoExplore.currentTarget) : 0; const stuckCheck = autoExplore.currentTarget && currentDistSq === autoExplore.lastDistToTargetSq; if (stuckCheck) { autoExplore.stuckCounter = (autoExplore.stuckCounter || 0) + 1; } else { autoExplore.stuckCounter = 0; } autoExplore.lastDistToTargetSq = currentDistSq; // Pick new target if: no target, timeout, or stuck if (!autoExplore.currentTarget || now - autoExplore.lastTargetTime > autoExplore.targetCooldown || autoExplore.stuckCounter > 60) { // Try to find an unexplored area with resources let foundTarget = false; // v7.73: Search for any resource using distanceToSquared() // v8.08: forEach to for loop let anyResource = null; let anyResourceDistSq = Infinity; for (let i = 0; i < worldState.interactables.length; i++) { const obj = worldState.interactables[i]; if (!obj.parent) continue; const distSq = player.position.distanceToSquared(obj.position); if (distSq > AUTO_EXPLORE_THRESHOLDS.minExplore && distSq < anyResourceDistSq) { anyResourceDistSq = distSq; anyResource = obj; } } if (anyResource && anyResourceDistSq < AUTO_EXPLORE_THRESHOLDS.maxExplore) { // v8.24: Use pooled target vector instead of clone() if (!autoExplore._targetVec) autoExplore._targetVec = new THREE.Vector3(); autoExplore._targetVec.copy(anyResource.position); autoExplore.currentTarget = autoExplore._targetVec; foundTarget = true; } // If no resources found, pick a random direction if (!foundTarget) { // v8.24: Use pooled target vector instead of new Vector3() if (!autoExplore._targetVec) autoExplore._targetVec = new THREE.Vector3(); const angle = Math.random() * Math.PI * 2; const distance = 20 + Math.random() * 20; autoExplore._targetVec.set( player.position.x + Math.cos(angle) * distance, 0, player.position.z + Math.sin(angle) * distance ); autoExplore.currentTarget = autoExplore._targetVec; } // Clamp to world bounds autoExplore.currentTarget.x = Math.max(-45, Math.min(45, autoExplore.currentTarget.x)); autoExplore.currentTarget.z = Math.max(-45, Math.min(45, autoExplore.currentTarget.z)); autoExplore.lastTargetTime = now; autoExplore.stuckCounter = 0; } worldState.target = autoExplore.currentTarget; // v7.73: Check if reached target using distanceToSquared() (3^2 = 9) if (autoExplore.currentTarget && player.position.distanceToSquared(autoExplore.currentTarget) < 9) { autoExplore.currentTarget = null; } return true; } // ============================================ // v6.68: UNIFIED AI BEHAVIOR SYSTEM // Central controller for all autonomous behaviors // ============================================ const AI_BEHAVIOR = { current: 'explorer', // manual, explorer, pusher, miner, defender, builder, hunter, trader behaviors: { manual: { name: 'Manual Control', icon: '🎮', color: '#888888', description: 'Full player control - no AI assistance' }, explorer: { name: 'Explorer', icon: '🔍', color: '#00ff88', description: 'Auto-gathering resources and fighting mobs' }, pusher: { name: 'Lane Pusher', icon: '⚔️', color: '#ff4444', description: 'Aggressively pushing lanes and destroying towers' }, // v7.33: NEW DOTA-STYLE STRATEGIC AI STATES waveCoordinator: { name: 'Wave Coordinator', icon: '🌊', color: '#00ddff', description: 'Waits for creep waves, never attacks towers without cover' }, towerDiver: { name: 'Tower Diver', icon: '💀', color: '#ff0066', description: 'Aggressive dives, manages tower aggro like a pro' }, splitPusher: { name: 'Split Pusher', icon: '🔀', color: '#ff8800', description: 'Creates pressure across multiple lanes simultaneously' }, lastHitter: { name: 'Last Hitter', icon: '💰', color: '#ffdd00', description: 'Focuses on last-hitting creeps for maximum gold/XP' }, siegeMaster: { name: 'Siege Master', icon: '🏰', color: '#aa44ff', description: 'Patient tower destruction with perfect wave timing' }, gankHunter: { name: 'Gank Hunter', icon: '🗡️', color: '#ff0000', description: 'Roams between lanes hunting enemy heroes' }, miner: { name: 'Miner', icon: '⛏️', color: '#ffaa00', description: 'Focus on gathering ore, logs, and crystals' }, defender: { name: 'Defender', icon: '🛡️', color: '#4488ff', description: 'Protect base, ship, and friendly creeps' }, terraformer: { name: 'Terraformer', icon: '🚜', color: '#cd853f', // v6.82: Improved contrast description: 'Scan and smooth rough terrain for construction' }, builder: { name: 'Builder', icon: '🔨', color: '#00bfff', description: 'Build structures at construction sites with 100% efficiency' }, hunter: { name: 'Hunter', icon: '🎯', color: '#ff0088', description: 'Aggressive mob hunting for XP and gold' }, trader: { name: 'Trader', icon: '💰', color: '#ffd700', description: 'Gather and sell for profit, exploit market events' }, // v7.3: ADVANCED AI BEHAVIORS - Mind-blowing autonomous systems evolutionary: { name: 'Evolutionary Architect', icon: '🧬', color: '#00ff99', description: 'Learns from failures, evolves strategies each generation' }, hivemind: { name: 'Hive Mind Swarm', icon: '🌀', color: '#ff00ff', description: 'Splits into 5 ghost drones moving in murmuration patterns' }, temporal: { name: 'Temporal Echo', icon: '⏱️', color: '#00ffff', description: 'Creates time-delayed ghost copies replaying past actions' }, chaos: { name: 'Chaos Agent', icon: '🎭', color: '#ff6600', description: 'Does the OPPOSITE of optimal - discovers hidden mechanics' }, precog: { name: 'Precognition', icon: '🔮', color: '#aa00ff', description: 'Predicts enemy spawns & danger zones 30s ahead' }, fluid: { name: 'Fluid Dynamics', icon: '🌊', color: '#0088ff', description: 'Water-like movement, flows around obstacles naturally' }, jester: { name: 'Jester Protocol', icon: '🎪', color: '#ffff00', description: 'Prioritizes "interesting" over "efficient" - creates surprises' }, lightning: { name: 'Lightning Router', icon: '⚡', color: '#ffff88', description: 'Calculates mathematically optimal path through ALL objectives' }, shadow: { name: 'Shadow Stalker', icon: '🌑', color: '#440066', description: 'Moves only when unobserved, hugs darkness, stealth predator' }, rhythm: { name: 'Rhythmic Conductor', icon: '🎼', color: '#ff88ff', description: 'Actions sync to internal beat, creates music from gameplay' } }, stats: { resourcesGathered: 0, mobsKilled: 0, goldEarned: 0, structuresBuilt: 0, towersDestroyed: 0, damageBlocked: 0 } }; // Set AI behavior mode function setAIBehavior(behaviorId) { const oldBehavior = AI_BEHAVIOR.current; AI_BEHAVIOR.current = behaviorId; // Disable all specific AI systems first autoExplore.enabled = false; LANE_PUSH_AI.enabled = false; // Enable the appropriate system switch (behaviorId) { case 'manual': showNotification('🎮 MANUAL: Full control is yours!', 'info'); break; case 'explorer': autoExplore.enabled = true; showNotification('🔍 EXPLORER: Auto-gathering and combat', 'info'); break; case 'pusher': LANE_PUSH_AI.enabled = true; LANE_PUSH_AI.state = 'idle'; LANE_PUSH_AI.currentLane = null; showNotification('⚔️ PUSHER: Destroying enemy towers!', 'warning'); break; // v7.33: NEW DOTA-STYLE AI STATES case 'waveCoordinator': DOTA_AI.init('waveCoordinator'); showNotification('🌊 WAVE COORDINATOR: Perfect wave timing engaged!', 'success'); addCopilotMessage('🌊 Wave Coordinator active! I will NEVER attack towers without creep cover. Watching wave timings...', 'ai'); break; case 'towerDiver': DOTA_AI.init('towerDiver'); showNotification('💀 TOWER DIVER: Aggressive plays, calculated aggro!', 'warning'); addCopilotMessage('💀 Tower Diver engaged! I will dive towers aggressively but manage aggro like a pro. High risk, high reward!', 'ai'); break; case 'splitPusher': DOTA_AI.init('splitPusher'); showNotification('🔀 SPLIT PUSHER: Multi-lane pressure!', 'success'); addCopilotMessage('🔀 Split Pusher active! Creating pressure across ALL lanes. The enemy can\'t defend everywhere!', 'ai'); break; case 'lastHitter': DOTA_AI.init('lastHitter'); showNotification('💰 LAST HITTER: Maximum efficiency farming!', 'info'); addCopilotMessage('💰 Last Hitter mode! I will only attack creeps when they\'re low HP for maximum gold. Patience is profit!', 'ai'); break; case 'siegeMaster': DOTA_AI.init('siegeMaster'); showNotification('🏰 SIEGE MASTER: Patient tower destruction!', 'success'); addCopilotMessage('🏰 Siege Master active! I will wait for PERFECT wave timing before touching any tower. No backdoor penalties!', 'ai'); break; case 'gankHunter': DOTA_AI.init('gankHunter'); showNotification('🗡️ GANK HUNTER: Roaming for kills!', 'warning'); addCopilotMessage('🗡️ Gank Hunter engaged! Roaming between lanes hunting the enemy hero. They won\'t see me coming!', 'ai'); break; case 'miner': // Miner uses its own AI, not explorer showNotification('⛏️ MINER: Focusing on resource gathering', 'info'); break; case 'defender': showNotification('🛡️ DEFENDER: Protecting base and allies', 'info'); break; case 'terraformer': showNotification('🚜 TERRAFORMER: Smoothing terrain for construction', 'info'); break; case 'builder': showNotification('🔨 BUILDER: Building at construction sites', 'info'); break; case 'hunter': showNotification('🎯 HUNTER: Hunting mobs aggressively', 'warning'); break; case 'trader': showNotification('💰 TRADER: Maximizing profits', 'info'); break; // v7.3: ADVANCED AI BEHAVIORS case 'evolutionary': EVOLUTIONARY_AI.init(); showNotification('🧬 EVOLUTIONARY: Learning from every failure...', 'success'); break; case 'hivemind': HIVEMIND_AI.init(); showNotification('🌀 HIVEMIND: Consciousness fragmenting into swarm...', 'success'); break; case 'temporal': TEMPORAL_AI.init(); showNotification('⏱️ TEMPORAL: Recording timeline echoes...', 'success'); break; case 'chaos': CHAOS_AI.init(); showNotification('🎭 CHAOS: Embracing beautiful disorder!', 'warning'); break; case 'precog': PRECOG_AI.init(); showNotification('🔮 PRECOG: Future sight activating...', 'success'); break; case 'fluid': FLUID_AI.init(); showNotification('🌊 FLUID: Becoming one with the flow...', 'success'); break; case 'jester': JESTER_AI.init(); showNotification('🎪 JESTER: Time to make some chaos-art!', 'warning'); break; case 'lightning': LIGHTNING_AI.init(); showNotification('⚡ LIGHTNING: Calculating optimal path matrix...', 'success'); break; case 'shadow': SHADOW_AI.init(); showNotification('🌑 SHADOW: Merging with the darkness...', 'info'); break; case 'rhythm': RHYTHM_AI.init(); showNotification('🎼 RHYTHM: Feel the beat, become the music!', 'success'); break; } updateAIBehaviorUI(); AudioSystem.buttonClick && AudioSystem.buttonClick(); } // Update AI behavior UI function updateAIBehaviorUI() { const select = document.getElementById('ai-behavior-select'); const status = document.getElementById('ai-behavior-status'); const details = document.getElementById('ai-behavior-details'); const behavior = AI_BEHAVIOR.behaviors[AI_BEHAVIOR.current]; if (!behavior) return; if (select) select.value = AI_BEHAVIOR.current; if (status) { let statusText = `${behavior.icon} ${behavior.name.toUpperCase()}`; // Add state-specific info if (AI_BEHAVIOR.current === 'pusher' && LANE_PUSH_AI.currentLane) { statusText += ` - ${LANE_PUSH_AI.currentLane.toUpperCase()}`; } else if (AI_BEHAVIOR.current === 'explorer' && autoExplore.state) { statusText += ` - ${autoExplore.state.toUpperCase()}`; } else if (AI_BEHAVIOR.current === 'defender' && DEFENDER_AI.state) { statusText += ` - ${DEFENDER_AI.state.toUpperCase()}`; } else if (AI_BEHAVIOR.current === 'miner' && MINER_AI.state) { statusText += ` - ${MINER_AI.state.toUpperCase()}`; } else if (AI_BEHAVIOR.current === 'terraformer' && TERRAFORMER_AI.state) { statusText += ` - ${TERRAFORMER_AI.state.toUpperCase()}`; } else if (AI_BEHAVIOR.current === 'builder' && BUILDER_AI.state) { statusText += ` - ${BUILDER_AI.state.toUpperCase()}`; } status.textContent = statusText; status.style.color = behavior.color; status.style.background = `${behavior.color}22`; } if (details) { details.textContent = behavior.description; } } // ============================================ // v6.68: MINER AI - Focus on resource gathering // ============================================ const MINER_AI = { state: 'idle', // idle, mining, returning, selling targetResource: null, preferredResources: ['rock', 'ore', 'crystal', 'tree'], // Priority order gatherRange: 80, // Search range for resources inventoryThreshold: 15, // Return to sell when inventory this full lastDecisionTime: 0, decisionInterval: 300, stats: { oreGathered: 0, logsGathered: 0, crystalsGathered: 0 } }; function runMinerAI(dt) { if (AI_BEHAVIOR.current !== 'miner' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); if (now - MINER_AI.lastDecisionTime < MINER_AI.decisionInterval) return true; MINER_AI.lastDecisionTime = now; // Check inventory - if full, return to ship to sell if (gameData.inventory.length >= MINER_AI.inventoryThreshold) { MINER_AI.state = 'returning'; // Head to ship - v7.78: distanceToSquared optimization if (SHIP_STATE.mesh) { const distToShipSq = player.position.distanceToSquared(SHIP_STATE.mesh.position); if (distToShipSq > 25) { // 5*5=25 // v7.86: Use setWorldTarget instead of clone() setWorldTarget(SHIP_STATE.mesh.position); return true; } else { // At ship - try to sell resources MINER_AI.state = 'selling'; autoSellResources(); return true; } } } // Priority: Find and mine resources // v8.08: forEach to for loop + pre-compute squared ranges MINER_AI.state = 'mining'; let bestResource = null; let bestScore = -Infinity; const gatherRangeSq = MINER_AI.gatherRange * MINER_AI.gatherRange; for (let i = 0; i < worldState.interactables.length; i++) { const obj = worldState.interactables[i]; if (!obj.parent || !obj.userData) continue; const type = obj.userData.type; const name = (obj.userData.name || '').toLowerCase(); // Score resources by priority let score = 0; if (type === 'rock' || name.includes('rock') || name.includes('ore')) { score = 100; MINER_AI.targetType = 'ore'; } else if (name.includes('crystal')) { score = 120; // Crystals are valuable MINER_AI.targetType = 'crystal'; } else if (type === 'tree' || name.includes('tree')) { score = 50; MINER_AI.targetType = 'log'; } else { continue; // Not a mineable resource } // v7.80: distanceToSquared optimization const distSq = player.position.distanceToSquared(obj.position); if (distSq > gatherRangeSq) continue; const dist = Math.sqrt(distSq); // Only sqrt for score calculation // Closer is better score -= dist * 0.5; if (score > bestScore) { bestScore = score; bestResource = obj; } } if (bestResource) { MINER_AI.targetResource = bestResource; // v7.80: distanceToSquared optimization const distSq = player.position.distanceToSquared(bestResource.position); const interactionRangeSq = CONFIG.INTERACTION_RANGE * CONFIG.INTERACTION_RANGE; if (distSq > interactionRangeSq) { // v7.86: Use setWorldTarget instead of clone() setWorldTarget(bestResource.position); worldState.interactTarget = bestResource; } else { worldState.target = null; if (now - worldState.lastActionTime > CONFIG.INTERACTION_COOLDOWN) { performAction(bestResource); worldState.lastActionTime = now; MINER_AI.stats[MINER_AI.targetType + 'Gathered'] = (MINER_AI.stats[MINER_AI.targetType + 'Gathered'] || 0) + 1; } } return true; } // No resources nearby - explore to find more - v7.78: distanceToSquared optimization MINER_AI.state = 'exploring'; if (!autoExplore.currentTarget || player.position.distanceToSquared(autoExplore.currentTarget) < 25) { // 5*5=25 // v7.88: Use pooled exploration target vector if (!autoExplore._explorationTarget) autoExplore._explorationTarget = new THREE.Vector3(); autoExplore._explorationTarget.set( (Math.random() - 0.5) * 80, player.position.y, (Math.random() - 0.5) * 80 ); autoExplore.currentTarget = autoExplore._explorationTarget; } worldState.target = autoExplore.currentTarget; return true; } // Auto-sell resources to best merchant function autoSellResources() { const resourcesToSell = ['Ore', 'Log', 'Crystal', 'Slime', 'Chitin']; let soldSomething = false; for (const resource of resourcesToSell) { const count = getItemCount(resource); if (count > 5) { // Keep 5 of each const sellCount = count - 5; // Find best merchant for this resource let bestMerchant = 'grimjaw'; // Default let bestPrice = 0; for (const [id, merchant] of Object.entries(MERCHANTS)) { const price = getMerchantBuyPrice(id, resource); if (price > bestPrice && merchant.gold >= price) { bestPrice = price; bestMerchant = id; } } if (bestPrice > 0) { sellToMerchant(bestMerchant, resource, sellCount); soldSomething = true; } } } if (soldSomething) { showNotification('💰 Auto-sold resources!', 'success'); } } // ============================================ // v6.68: DEFENDER AI - Protect base and allies // ============================================ const DEFENDER_AI = { state: 'idle', // idle, patrolling, engaging, retreating, healing defenseRadius: 40, // Stay within this radius of ship lastDecisionTime: 0, decisionInterval: 250, targetEnemy: null, stats: { enemiesRepelled: 0, alliesHealed: 0, damageBlocked: 0 }, // v7.88: Pre-allocated fallback position vector _fallbackShipPos: null, // v7.88: Pre-allocated patrol target vector _patrolTarget: null }; function runDefenderAI(dt) { if (AI_BEHAVIOR.current !== 'defender' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); if (now - DEFENDER_AI.lastDecisionTime < DEFENDER_AI.decisionInterval) return true; DEFENDER_AI.lastDecisionTime = now; // v7.78: distanceToSquared optimization for defender AI // v7.88: Use pooled fallback vector instead of new allocation if (!DEFENDER_AI._fallbackShipPos) DEFENDER_AI._fallbackShipPos = new THREE.Vector3(0, 0, 0); const shipPos = SHIP_STATE.mesh ? SHIP_STATE.mesh.position : DEFENDER_AI._fallbackShipPos; const distToShipSq = player.position.distanceToSquared(shipPos); const defenseRadiusSq = DEFENDER_AI.defenseRadius * DEFENDER_AI.defenseRadius; // Priority 1: Engage enemies near the base let nearestThreat = null; let nearestThreatDistSq = Infinity; // Check mobs - v8.01: forEach to for loop conversion for (let i = 0, len = worldState.mobs.length; i < len; i++) { const mob = worldState.mobs[i]; if (!mob.parent) continue; const distToBaseSq = mob.position.distanceToSquared(shipPos); if (distToBaseSq < defenseRadiusSq) { const distToPlayerSq = player.position.distanceToSquared(mob.position); if (distToPlayerSq < nearestThreatDistSq) { nearestThreatDistSq = distToPlayerSq; nearestThreat = mob; } } } // Check hostile creeps - v8.01: forEach to for loop conversion if (typeof creepWaveState !== 'undefined' && creepWaveState.creeps) { for (let i = 0, len = creepWaveState.creeps.length; i < len; i++) { const creep = creepWaveState.creeps[i]; if (!creep.parent || creep.userData.team !== 'B') continue; const distToBaseSq = creep.position.distanceToSquared(shipPos); if (distToBaseSq < defenseRadiusSq) { const distToPlayerSq = player.position.distanceToSquared(creep.position); if (distToPlayerSq < nearestThreatDistSq) { nearestThreatDistSq = distToPlayerSq; nearestThreat = creep; } } } } if (nearestThreat) { DEFENDER_AI.state = 'engaging'; DEFENDER_AI.targetEnemy = nearestThreat; const interactRangeSq = CONFIG.INTERACTION_RANGE * CONFIG.INTERACTION_RANGE; // v7.78 if (nearestThreatDistSq > interactRangeSq) { // v7.86: Use setWorldTarget instead of clone() setWorldTarget(nearestThreat.position); worldState.interactTarget = nearestThreat; } else { worldState.target = null; if (now - worldState.lastActionTime > CONFIG.INTERACTION_COOLDOWN) { performAction(nearestThreat); worldState.lastActionTime = now; } } return true; } // Priority 2: Stay near the ship if too far if (distToShipSq > defenseRadiusSq) { DEFENDER_AI.state = 'returning'; // v7.86: Use setWorldTarget instead of clone() setWorldTarget(shipPos); return true; } // Priority 3: Patrol around the base DEFENDER_AI.state = 'patrolling'; // Circular patrol around ship const patrolAngle = (now / 5000) % (Math.PI * 2); const patrolRadius = DEFENDER_AI.defenseRadius * 0.6; // v7.88: Use pooled patrol target vector if (!DEFENDER_AI._patrolTarget) DEFENDER_AI._patrolTarget = new THREE.Vector3(); DEFENDER_AI._patrolTarget.set( shipPos.x + Math.cos(patrolAngle) * patrolRadius, player.position.y, shipPos.z + Math.sin(patrolAngle) * patrolRadius ); worldState.target = DEFENDER_AI._patrolTarget; return true; } // ============================================ // v6.82: TERRAFORMER AI - Smooth terrain like agents // Uses same logic as agent terraformers: scan, smooth, prepare sites // ============================================ const TERRAFORMER_AI = { state: 'idle', // idle, scanning, smoothing, moving targetSite: null, lastDecisionTime: 0, decisionInterval: 100, // v9.4: Faster smoothing (was 300) smoothRadius: 5, // v9.4: Larger radius for more visible effect (was 3) lastNotification: 0, // v6.82: Throttle notifications notificationCooldown: 3000, // Only show same notification every 3s stats: { sitesSmoothed: 0, beaconsPlaced: 0 }, // v7.88: Pre-allocated wander target vector _wanderTarget: null }; // Calculate terrain roughness in an area (same algorithm as agents) function calculateTerrainRoughness(cx, cz, radius) { let heights = []; for (let dx = -radius; dx <= radius; dx++) { for (let dz = -radius; dz <= radius; dz++) { const tx = cx + dx, tz = cz + dz; if (worldState.terrain[tx] && worldState.terrain[tx][tz] !== undefined && worldState.terrain[tx][tz] > 0) { heights.push(worldState.terrain[tx][tz]); } } } if (heights.length < 2) return 0; const avg = heights.reduce((a, b) => a + b, 0) / heights.length; const variance = heights.reduce((sum, h) => sum + Math.pow(h - avg, 2), 0) / heights.length; return Math.sqrt(variance); } // Smooth terrain at player position (same algorithm as agents) // v9.4: Actually updates the 3D terrain mesh visuals function smoothTerrainAtPosition(worldX, worldZ) { const tileX = Math.floor((worldX / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); const tileZ = Math.floor((worldZ / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); const smoothRadius = TERRAFORMER_AI.smoothRadius; let totalHeight = 0, count = 0; let heightMap = []; for (let dx = -smoothRadius; dx <= smoothRadius; dx++) { for (let dz = -smoothRadius; dz <= smoothRadius; dz++) { const tx = tileX + dx, tz = tileZ + dz; if (worldState.terrain[tx] && worldState.terrain[tx][tz] !== undefined && worldState.terrain[tx][tz] > 0) { const h = worldState.terrain[tx][tz]; totalHeight += h; count++; heightMap.push({ tx, tz, h }); } } } if (count > 0) { const avgHeight = totalHeight / count; for (const cell of heightMap) { // Smooth 95% toward average for more visible effect worldState.terrain[cell.tx][cell.tz] = cell.h + (avgHeight - cell.h) * 0.95; } // v9.4: Update the 3D terrain mesh to show the smoothed terrain if (typeof worldState.updateTerrainMeshes === 'function') { worldState.updateTerrainMeshes(tileX, tileZ, smoothRadius + 1); } return true; } return false; } // Find rough terrain to smooth function findRoughTerrainNearby(player, searchRadius) { const playerTileX = Math.floor((player.position.x / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); const playerTileZ = Math.floor((player.position.z / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); let bestSite = null; let highestRoughness = 0.3; // Minimum threshold // Scan in a spiral pattern from player for (let dist = 5; dist < searchRadius; dist += 5) { for (let angle = 0; angle < Math.PI * 2; angle += Math.PI / 4) { const checkX = playerTileX + Math.floor(Math.cos(angle) * dist); const checkZ = playerTileZ + Math.floor(Math.sin(angle) * dist); const roughness = calculateTerrainRoughness(checkX, checkZ, 3); if (roughness > highestRoughness) { highestRoughness = roughness; bestSite = { tileX: checkX, tileZ: checkZ, worldX: (checkX - CONFIG.WORLD_SIZE / 2) * CONFIG.TILE_SIZE, worldZ: (checkZ - CONFIG.WORLD_SIZE / 2) * CONFIG.TILE_SIZE, roughness: roughness }; } } } return bestSite; } function runTerraformerAI(dt) { if (AI_BEHAVIOR.current !== 'terraformer' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); if (now - TERRAFORMER_AI.lastDecisionTime < TERRAFORMER_AI.decisionInterval) return true; TERRAFORMER_AI.lastDecisionTime = now; const playerTileX = Math.floor((player.position.x / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); const playerTileZ = Math.floor((player.position.z / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); // Check roughness at current position const currentRoughness = calculateTerrainRoughness(playerTileX, playerTileZ, TERRAFORMER_AI.smoothRadius); // Priority 1: Smooth rough terrain at current location if (currentRoughness > 0.2) { TERRAFORMER_AI.state = 'smoothing'; if (smoothTerrainAtPosition(player.position.x, player.position.z)) { TERRAFORMER_AI.stats.sitesSmoothed++; // Throttle smoothing notifications if (now - TERRAFORMER_AI.lastNotification > TERRAFORMER_AI.notificationCooldown) { TERRAFORMER_AI.lastNotification = now; showNotification(`🚜 Smoothed terrain (roughness: ${currentRoughness.toFixed(2)})`, 'info'); } // Create visual effect if (typeof spawnTerraformParticles === 'function') { spawnTerraformParticles(player.position); } // Place construction beacon if terrain is now smooth enough const newRoughness = calculateTerrainRoughness(playerTileX, playerTileZ, TERRAFORMER_AI.smoothRadius); if (newRoughness < 0.15) { // Initialize construction sites array if needed if (!worldState.constructionSites) worldState.constructionSites = []; // Check if a beacon already exists nearby const nearbyBeacon = worldState.constructionSites.find(site => Math.abs(site.position.x - player.position.x) < 10 && Math.abs(site.position.z - player.position.z) < 10 ); if (!nearbyBeacon) { worldState.constructionSites.push({ position: player.position.clone(), createdAt: now, claimed: false, buildProgress: 0, type: 'terraformed' }); TERRAFORMER_AI.stats.beaconsPlaced++; showNotification('📍 Construction beacon placed!', 'success'); } } } return true; } // Priority 2: Find rough terrain nearby TERRAFORMER_AI.state = 'scanning'; const roughSite = findRoughTerrainNearby(player, 50); if (roughSite) { TERRAFORMER_AI.targetSite = roughSite; TERRAFORMER_AI.state = 'moving'; // v7.88: Use pooled wander target vector if (!TERRAFORMER_AI._wanderTarget) TERRAFORMER_AI._wanderTarget = new THREE.Vector3(); TERRAFORMER_AI._wanderTarget.set(roughSite.worldX, 0, roughSite.worldZ); worldState.target = TERRAFORMER_AI._wanderTarget; return true; } // Priority 3: Random exploration to find rough terrain TERRAFORMER_AI.state = 'exploring'; const wanderAngle = Math.random() * Math.PI * 2; const wanderDist = 20 + Math.random() * 30; // v7.88: Use pooled wander target vector instead of new allocation if (!TERRAFORMER_AI._wanderTarget) TERRAFORMER_AI._wanderTarget = new THREE.Vector3(); TERRAFORMER_AI._wanderTarget.set( player.position.x + Math.cos(wanderAngle) * wanderDist, 0, player.position.z + Math.sin(wanderAngle) * wanderDist ); worldState.target = TERRAFORMER_AI._wanderTarget; return true; } // ============================================ // v6.82: BUILDER AI - Enhanced to match agent capabilities // Seeks construction beacons, builds with 100% efficiency // ============================================ const BUILDER_AI = { state: 'idle', // idle, seeking, building, repairing, gathering currentSite: null, lastDecisionTime: 0, decisionInterval: 400, buildRange: 5, lastNotification: 0, // v6.82: Throttle notifications notificationCooldown: 3000, // Only show same notification every 3s lastBuildNotify: 0, // v6.82: Separate cooldown for building progress stats: { structuresBuilt: 0, repairsDone: 0, sitesCompleted: 0 } }; function runBuilderAI(dt) { if (AI_BEHAVIOR.current !== 'builder' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); if (now - BUILDER_AI.lastDecisionTime < BUILDER_AI.decisionInterval) return true; BUILDER_AI.lastDecisionTime = now; // Priority 1: Build at construction beacons (placed by terraformer) if (worldState.constructionSites && worldState.constructionSites.length > 0) { // Find unclaimed or self-claimed sites const availableSites = worldState.constructionSites.filter(site => !site.claimed || site.claimed === 'player' ); if (availableSites.length > 0) { // Sort by distance - v7.78: distanceToSquared optimization availableSites.sort((a, b) => { const distASq = player.position.distanceToSquared(a.position); const distBSq = player.position.distanceToSquared(b.position); return distASq - distBSq; }); const targetSite = availableSites[0]; // v8.0: Use distanceToSquared for buildRange comparison const distToSiteSq = player.position.distanceToSquared(targetSite.position); const buildRangeSq = BUILDER_AI.buildRange * BUILDER_AI.buildRange; if (distToSiteSq <= buildRangeSq) { // At site - BUILD! BUILDER_AI.state = 'building'; targetSite.claimed = 'player'; if (!targetSite.buildProgress) targetSite.buildProgress = 0; targetSite.buildProgress += 15; // Faster building than agents if (targetSite.buildProgress >= 100) { // Construction complete! BUILDER_AI.stats.sitesCompleted++; showNotification('🏗️ Construction complete! 100% efficiency!', 'success'); // Create actual structure at site if (typeof queueConstruction === 'function') { const structures = ['turret', 'wall', 'barracks']; const structureType = structures[Math.floor(Math.random() * structures.length)]; queueConstruction(structureType, targetSite.position); BUILDER_AI.stats.structuresBuilt++; } // Remove beacon worldState.constructionSites = worldState.constructionSites.filter(s => s !== targetSite); } else { // Throttle build progress notifications (every 25%) if (now - BUILDER_AI.lastBuildNotify > 2000 || targetSite.buildProgress % 25 < 15) { BUILDER_AI.lastBuildNotify = now; } } return true; } else { // Move to site BUILDER_AI.state = 'seeking'; BUILDER_AI.currentSite = targetSite; // v7.86: Use setWorldTarget instead of clone() setWorldTarget(targetSite.position); return true; } } } // Priority 2: Check for damaged structures to repair // v8.0: Use distanceToSquared for buildRange comparison if (typeof baseBuildingState !== 'undefined' && baseBuildingState.buildings) { for (const building of baseBuildingState.buildings) { if (building.hp < building.maxHp * 0.5) { BUILDER_AI.state = 'repairing'; const distSq = player.position.distanceToSquared(building.mesh.position); if (distSq > buildRangeSq) { // v8.0: reuse buildRangeSq from above // v7.86: Use setWorldTarget instead of clone() setWorldTarget(building.mesh.position); } else { if (typeof repairBuilding === 'function') { repairBuilding(building); BUILDER_AI.stats.repairsDone++; // Throttle repair notifications if (now - BUILDER_AI.lastNotification > BUILDER_AI.notificationCooldown) { BUILDER_AI.lastNotification = now; showNotification('🔧 Repairing structure...', 'info'); } } } return true; } } } // Priority 3: If no beacons exist, gather resources // (Encourages using terraformer first to prepare sites) BUILDER_AI.state = 'gathering'; // Throttle gathering notification if (now - BUILDER_AI.lastNotification > BUILDER_AI.notificationCooldown * 2) { BUILDER_AI.lastNotification = now; showNotification('🔍 Searching for construction sites...', 'info'); } return runMinerAI(dt); } // ============================================ // v6.68: HUNTER AI - Aggressive mob hunting // ============================================ const HUNTER_AI = { state: 'idle', // idle, hunting, engaging, retreating targetMob: null, huntRange: 100, // Search far for targets lastDecisionTime: 0, decisionInterval: 200, // Fast decisions for combat retreatThreshold: 0.2, // Retreat at 20% HP stats: { mobsHunted: 0, elitesKilled: 0, bossesKilled: 0 } }; // v7.73: Pre-computed squared thresholds for HunterAI const HUNTER_AI_THRESHOLDS = { huntRangeSq: 100 * 100, // 10000 interactionSq: null, // Set from CONFIG at runtime abilityRangeSq: 10 * 10, // 100 exploreTargetSq: 5 * 5 // 25 }; function runHunterAI(dt) { if (AI_BEHAVIOR.current !== 'hunter' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); if (now - HUNTER_AI.lastDecisionTime < HUNTER_AI.decisionInterval) return true; HUNTER_AI.lastDecisionTime = now; // v7.73: Cache interaction range squared if (HUNTER_AI_THRESHOLDS.interactionSq === null) { HUNTER_AI_THRESHOLDS.interactionSq = CONFIG.INTERACTION_RANGE * CONFIG.INTERACTION_RANGE; } const interactRangeSq = HUNTER_AI_THRESHOLDS.interactionSq; // v8.26: Guard against undefined gameData.player if (!gameData?.player?.hp || !gameData?.player?.maxHp) return true; const playerHpPercent = gameData.player.hp / gameData.player.maxHp; // Retreat if low HP if (playerHpPercent < HUNTER_AI.retreatThreshold) { HUNTER_AI.state = 'retreating'; if (SHIP_STATE.mesh) { // v7.86: Use setWorldTarget instead of clone() setWorldTarget(SHIP_STATE.mesh.position); } return true; } // Use abilities aggressively useHunterAbilities(now); // v7.73: Find best target using distanceToSquared() let bestTarget = null; let bestScore = -Infinity; let bestTargetDistSq = Infinity; // v8.02: forEach to for loop conversion for hot path const mobs = worldState.mobs; const mobsLen = mobs.length; for (let i = 0; i < mobsLen; i++) { const mob = mobs[i]; if (!mob.parent || !mob.userData || mob.userData.hp <= 0) continue; let score = 100; // Prioritize elites and bosses if (mob.userData.isBoss) score += 500; else if (mob.userData.isElite) score += 200; // Higher XP rewards are better score += (mob.userData.xpReward || 50) * 0.5; // v7.73: Use distanceToSquared() - closer is better const distSq = player.position.distanceToSquared(mob.position); if (distSq > HUNTER_AI_THRESHOLDS.huntRangeSq) continue; // Approximate distance penalty using sqrt approximation for scoring only score -= Math.sqrt(distSq) * 0.3; // Lower HP targets are easier const hpPercent = mob.userData.hp / mob.userData.maxHp; score += (1 - hpPercent) * 50; if (score > bestScore) { bestScore = score; bestTarget = mob; bestTargetDistSq = distSq; } } if (bestTarget) { HUNTER_AI.state = 'engaging'; HUNTER_AI.targetMob = bestTarget; // v7.73: Use cached distSq for range check if (bestTargetDistSq > interactRangeSq) { // v7.86: Use setWorldTarget instead of clone() setWorldTarget(bestTarget.position); worldState.interactTarget = bestTarget; } else { worldState.target = null; if (now - worldState.lastActionTime > CONFIG.INTERACTION_COOLDOWN * 0.8) { // Attack faster performAction(bestTarget); worldState.lastActionTime = now; } } return true; } // No mobs - search for more HUNTER_AI.state = 'hunting'; // v7.73: Use distanceToSquared() for explore target check if (!autoExplore.currentTarget || player.position.distanceToSquared(autoExplore.currentTarget) < HUNTER_AI_THRESHOLDS.exploreTargetSq) { // Move to unexplored areas // v7.88: Use pooled exploration target vector if (!autoExplore._explorationTarget) autoExplore._explorationTarget = new THREE.Vector3(); autoExplore._explorationTarget.set( (Math.random() - 0.5) * 100, player.position.y, (Math.random() - 0.5) * 100 ); autoExplore.currentTarget = autoExplore._explorationTarget; } worldState.target = autoExplore.currentTarget; return true; } function useHunterAbilities(now) { // Aggressively use combat abilities const abilities = ['slash', 'whirlwind', 'dash', 'warcry']; for (const abilityKey of abilities) { if (abilityState[abilityKey]) { const ability = ABILITIES[abilityKey]; const lastUsed = abilityState[abilityKey].lastUsed || 0; const cooldown = ability?.cooldown || 5000; if (now - lastUsed > cooldown) { // v7.73: Check target nearby using distanceToSquared() if (HUNTER_AI.targetMob && worldState.player.position.distanceToSquared(HUNTER_AI.targetMob.position) < HUNTER_AI_THRESHOLDS.abilityRangeSq) { if (typeof useAbility === 'function') { useAbility(abilityKey); } break; // One ability per tick } } } } } // ============================================ // v6.68: TRADER AI - Maximize profits // ============================================ const TRADER_AI = { state: 'idle', // idle, gathering, selling, waiting targetItem: null, lastDecisionTime: 0, decisionInterval: 1000, profitThreshold: 50, // Min profit to trigger trade stats: { totalProfit: 0, tradesCompleted: 0 } }; function runTraderAI(dt) { if (AI_BEHAVIOR.current !== 'trader' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); if (now - TRADER_AI.lastDecisionTime < TRADER_AI.decisionInterval) return true; TRADER_AI.lastDecisionTime = now; // Check for profitable market events if (ECONOMY.activeEvents.length > 0) { for (const event of ECONOMY.activeEvents) { const eventData = MARKET_EVENTS[event.type]; // Find items affected by this event for (const [item, effect] of Object.entries(eventData.effects)) { if (item === 'ALL') continue; // If prices are HIGH - sell what we have if (effect > 0.3) { const count = getItemCount(item); if (count > 0) { TRADER_AI.state = 'selling'; // Find best merchant let bestMerchant = null; let bestPrice = 0; for (const [id, merchant] of Object.entries(MERCHANTS)) { const price = getMerchantBuyPrice(id, item); if (price > bestPrice && merchant.gold >= price) { bestPrice = price; bestMerchant = id; } } if (bestMerchant) { sellToMerchant(bestMerchant, item, count); TRADER_AI.stats.tradesCompleted++; showNotification(`💰 Sold ${count}x ${item} during ${eventData.name}!`, 'success'); } } } // If prices are LOW - gather that item if (effect < -0.2) { TRADER_AI.targetItem = item; TRADER_AI.state = 'gathering'; } } } } // Default: gather valuable resources if (TRADER_AI.state !== 'selling') { TRADER_AI.state = 'gathering'; return runMinerAI(dt); } return true; } // ════════════════════════════════════════════════════════════════════ // v7.3: ADVANCED AI BEHAVIORS - Mind-Blowing Autonomous Systems // ════════════════════════════════════════════════════════════════════ // 1. EVOLUTIONARY ARCHITECT - Learns from failures, evolves strategies // v7.87: Added pre-allocated vectors to avoid per-decision allocations const EVOLUTIONARY_AI = { state: 'evolving', generation: 1, dna: { aggression: 0.5, caution: 0.5, exploration: 0.5, efficiency: 0.5 }, fitness: 0, deathCount: 0, bestDNA: null, bestFitness: 0, mutations: [], lastDecisionTime: 0, // v7.87: Pre-allocated vectors to avoid per-decision allocations _exploreTarget: null, _fallbackTarget: null, // v7.88: Pooled geometry and materials for DNA helix spheres _helixSphereGeometry: null, _helixMaterial1: null, _helixMaterial2: null, init() { this.generation = 1; this.fitness = 0; this.deathCount = 0; this.dna = { aggression: Math.random(), caution: Math.random(), exploration: Math.random(), efficiency: Math.random() }; // v7.87: Initialize pre-allocated vectors if (!this._exploreTarget) { this._exploreTarget = new THREE.Vector3(); } if (!this._fallbackTarget) { this._fallbackTarget = new THREE.Vector3(0, 0, 0); } // v7.88: Initialize pooled geometry and materials for DNA helix if (!this._helixSphereGeometry) { this._helixSphereGeometry = new THREE.SphereGeometry(0.15); } if (!this._helixMaterial1) { this._helixMaterial1 = new THREE.MeshBasicMaterial({ color: 0x00ff99, transparent: true, opacity: 0.7 }); } if (!this._helixMaterial2) { this._helixMaterial2 = new THREE.MeshBasicMaterial({ color: 0xff00ff, transparent: true, opacity: 0.7 }); } this.spawnDNAVisual(); }, spawnDNAVisual() { // Create DNA helix visual effect // v7.88: Use pooled geometry and materials instead of creating 40 new ones if (this.helixMesh) scene.remove(this.helixMesh); const helixGroup = new THREE.Group(); for (let i = 0; i < 20; i++) { const t = i / 20 * Math.PI * 4; const sphere1 = new THREE.Mesh(this._helixSphereGeometry, this._helixMaterial1); sphere1.position.set(Math.sin(t) * 0.5, i * 0.3 - 3, Math.cos(t) * 0.5); const sphere2 = new THREE.Mesh(this._helixSphereGeometry, this._helixMaterial2); sphere2.position.set(-Math.sin(t) * 0.5, i * 0.3 - 3, -Math.cos(t) * 0.5); helixGroup.add(sphere1, sphere2); } this.helixMesh = helixGroup; }, mutate() { const gene = ['aggression', 'caution', 'exploration', 'efficiency'][Math.floor(Math.random() * 4)]; const change = (Math.random() - 0.5) * 0.3; this.dna[gene] = Math.max(0, Math.min(1, this.dna[gene] + change)); this.mutations.push({ gene, change, gen: this.generation }); showNotification(`🧬 Gen ${this.generation}: ${gene} mutated!`, 'info'); }, onDeath() { this.deathCount++; if (this.fitness > this.bestFitness) { this.bestFitness = this.fitness; this.bestDNA = { ...this.dna }; } this.generation++; this.mutate(); this.fitness = 0; showNotification(`🧬 Generation ${this.generation} begins!`, 'success'); } }; function runEvolutionaryAI(dt) { // v9.10: Skip DNA helix visual in customOnly worlds if (window.WORLD_SYSTEMS?.customOnly === true) return false; if (AI_BEHAVIOR.current !== 'evolutionary' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); if (now - EVOLUTIONARY_AI.lastDecisionTime < 200) return true; EVOLUTIONARY_AI.lastDecisionTime = now; // Update DNA helix position if (EVOLUTIONARY_AI.helixMesh) { EVOLUTIONARY_AI.helixMesh.position.copy(player.position); EVOLUTIONARY_AI.helixMesh.position.y += 3; EVOLUTIONARY_AI.helixMesh.rotation.y += 0.02; if (!EVOLUTIONARY_AI.helixMesh.parent) scene.add(EVOLUTIONARY_AI.helixMesh); } // Behavior based on DNA const dna = EVOLUTIONARY_AI.dna; let target = null; // High aggression: seek enemies - v7.78: distanceToSquared optimization if (dna.aggression > 0.6 && worldState.mobs.length > 0) { const nearestMob = worldState.mobs.reduce((nearest, mob) => { if (!mob.parent) return nearest; const distSq = player.position.distanceToSquared(mob.position); return (!nearest || distSq < nearest.distSq) ? { mob, distSq } : nearest; }, null); const aggroRangeSq = (30 * dna.aggression) * (30 * dna.aggression); if (nearestMob && nearestMob.distSq < aggroRangeSq) target = nearestMob.mob.position; } // High caution: avoid low health // v7.87: Use pre-allocated _fallbackTarget instead of new Vector3 if (dna.caution > 0.7 && playerState.health < playerState.maxHealth * 0.3) { if (!EVOLUTIONARY_AI._fallbackTarget) EVOLUTIONARY_AI._fallbackTarget = new THREE.Vector3(0, 0, 0); target = SHIP_STATE.mesh?.position || EVOLUTIONARY_AI._fallbackTarget; } // High exploration: wander far - v7.78: distanceToSquared optimization // v7.87: Use pre-allocated _exploreTarget instead of new Vector3 if (!target && dna.exploration > 0.5) { if (!EVOLUTIONARY_AI._exploreTarget) EVOLUTIONARY_AI._exploreTarget = new THREE.Vector3(); if (!EVOLUTIONARY_AI.exploreTarget || player.position.distanceToSquared(EVOLUTIONARY_AI.exploreTarget) < 25) { // 5*5=25 EVOLUTIONARY_AI._exploreTarget.set( (Math.random() - 0.5) * 150 * dna.exploration, player.position.y, (Math.random() - 0.5) * 150 * dna.exploration ); EVOLUTIONARY_AI.exploreTarget = EVOLUTIONARY_AI._exploreTarget; } target = EVOLUTIONARY_AI.exploreTarget; } // v7.86: Use setWorldTarget instead of clone() if (target) setWorldTarget(target); EVOLUTIONARY_AI.fitness += dt * 0.01; return true; } // 2. HIVE MIND SWARM - Splits into ghost drones moving in murmuration // v7.85: Added pre-allocated vectors to avoid O(n^2) allocations per frame const HIVEMIND_AI = { state: 'swarming', drones: [], droneCount: 5, lastDecisionTime: 0, centerOfMass: new THREE.Vector3(), swarmRadius: 8, // v7.85: Pre-allocated vectors for hot path optimization _toCenter: new THREE.Vector3(), _separation: new THREE.Vector3(), _diff: new THREE.Vector3(), _avgVel: new THREE.Vector3(), _noise: new THREE.Vector3(), // v7.88: Pooled geometries and materials for drone meshes _coreGeometry: null, _glowGeometry: null, _coreMaterials: null, _glowMaterials: null, init() { // v7.88: Initialize pooled geometries once if (!this._coreGeometry) { this._coreGeometry = new THREE.OctahedronGeometry(0.4); } if (!this._glowGeometry) { this._glowGeometry = new THREE.SphereGeometry(0.6); } // v7.88: Initialize pooled materials once (5 colors) if (!this._coreMaterials) { const colors = [0xff00ff, 0x00ffff, 0xffff00, 0xff8800, 0x88ff00]; this._coreMaterials = colors.map(c => new THREE.MeshBasicMaterial({ color: c, transparent: true, opacity: 0.6 })); this._glowMaterials = colors.map(c => new THREE.MeshBasicMaterial({ color: c, transparent: true, opacity: 0.2 })); } this.drones = []; for (let i = 0; i < this.droneCount; i++) { const drone = { offset: new THREE.Vector3( (Math.random() - 0.5) * this.swarmRadius, Math.random() * 2, (Math.random() - 0.5) * this.swarmRadius ), velocity: new THREE.Vector3(), mesh: this.createDroneMesh(i) }; this.drones.push(drone); scene.add(drone.mesh); } }, createDroneMesh(index) { // v7.88: Use pooled geometries and materials instead of creating new ones const group = new THREE.Group(); const colorIndex = index % 5; const core = new THREE.Mesh(this._coreGeometry, this._coreMaterials[colorIndex]); const glow = new THREE.Mesh(this._glowGeometry, this._glowMaterials[colorIndex]); group.add(core, glow); return group; }, cleanup() { this.drones.forEach(d => scene.remove(d.mesh)); this.drones = []; } }; function runHivemindAI(dt) { if (AI_BEHAVIOR.current !== 'hivemind' || mode !== 'world' || !worldState.player) { if (HIVEMIND_AI.drones.length > 0) HIVEMIND_AI.cleanup(); return false; } const player = worldState.player; const now = performance.now(); // Murmuration behavior - each drone follows rules // v7.85: Optimized to use pre-allocated vectors instead of 5+ allocations per drone per frame // v8.01: forEach to for loop conversion for hot path const droneCount = HIVEMIND_AI.drones.length; for (let i = 0; i < droneCount; i++) { const drone = HIVEMIND_AI.drones[i]; // Rule 1: Cohesion - move toward center - v7.85: use pre-allocated _toCenter HIVEMIND_AI._toCenter.copy(player.position).sub(drone.mesh.position).multiplyScalar(0.02); // Rule 2: Separation - avoid other drones - v7.85: use pre-allocated _separation, _diff // v8.12: Use lengthSq for initial distance check to avoid sqrt when dist >= 2 HIVEMIND_AI._separation.set(0, 0, 0); for (let j = 0; j < droneCount; j++) { if (i !== j) { const other = HIVEMIND_AI.drones[j]; HIVEMIND_AI._diff.copy(drone.mesh.position).sub(other.mesh.position); const distSq = HIVEMIND_AI._diff.lengthSq(); if (distSq < 4) { // 2*2 = 4 (squared threshold) const dist = Math.sqrt(distSq); // v8.12: Only compute sqrt when needed HIVEMIND_AI._separation.add(HIVEMIND_AI._diff.divideScalar(dist).multiplyScalar(0.5 / dist)); } } } // Rule 3: Alignment - match velocity of neighbors - v7.85: use pre-allocated _avgVel HIVEMIND_AI._avgVel.set(0, 0, 0); for (let j = 0; j < droneCount; j++) { HIVEMIND_AI._avgVel.add(HIVEMIND_AI.drones[j].velocity); } HIVEMIND_AI._avgVel.divideScalar(HIVEMIND_AI.droneCount).multiplyScalar(0.1); // Rule 4: Noise - random movement - v7.85: use pre-allocated _noise HIVEMIND_AI._noise.set( (Math.random() - 0.5) * 0.3, (Math.random() - 0.5) * 0.1, (Math.random() - 0.5) * 0.3 ); // Update velocity drone.velocity.add(HIVEMIND_AI._toCenter).add(HIVEMIND_AI._separation).add(HIVEMIND_AI._avgVel).add(HIVEMIND_AI._noise); drone.velocity.clampLength(0, 0.5); // Update position drone.mesh.position.add(drone.velocity); drone.mesh.rotation.y += 0.05; drone.mesh.rotation.x = Math.sin(now * 0.003 + i) * 0.3; } // Player follows center of swarm - v8.01: forEach to for loop HIVEMIND_AI.centerOfMass.set(0, 0, 0); for (let i = 0; i < droneCount; i++) { HIVEMIND_AI.centerOfMass.add(HIVEMIND_AI.drones[i].mesh.position); } HIVEMIND_AI.centerOfMass.divideScalar(HIVEMIND_AI.droneCount); // v7.86: Use setWorldTarget instead of clone() setWorldTarget(HIVEMIND_AI.centerOfMass); return true; } // 3. TEMPORAL ECHO - Creates time-delayed ghost copies const TEMPORAL_AI = { state: 'recording', timeline: [], maxHistory: 300, echoes: [], echoDelays: [30, 60, 90], // Frames of delay lastRecordTime: 0, // v7.88: Pre-allocated exploration target vector _exploreTarget: null, // v7.89: Pooled geometry/materials for echo meshes _pooledGeometry: null, _pooledMaterials: null, // v7.89: Pre-allocated vector pool for timeline positions _positionPool: null, _positionPoolIndex: 0, init() { this.timeline = []; this.cleanupEchoes(); // v7.89: Initialize pooled geometry once if (!this._pooledGeometry) { this._pooledGeometry = new THREE.CylinderGeometry(0.4, 0.4, 1.4, 8); } // v7.89: Initialize pooled materials once (3 colors) if (!this._pooledMaterials) { const colors = [0x00ffff, 0x0088ff, 0x0044aa]; this._pooledMaterials = colors.map((color, i) => new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.4 - i * 0.1, wireframe: true }) ); } // v7.89: Pre-allocate position pool for timeline if (!this._positionPool) { this._positionPool = []; for (let i = 0; i < this.maxHistory; i++) { this._positionPool.push(new THREE.Vector3()); } } this._positionPoolIndex = 0; this.echoDelays.forEach((delay, i) => { const echoMesh = this.createEchoMesh(i); this.echoes.push({ mesh: echoMesh, delay, index: 0 }); scene.add(echoMesh); }); }, createEchoMesh(index) { const group = new THREE.Group(); // v7.89: Use pooled geometry and material instead of creating new each time const body = new THREE.Mesh( this._pooledGeometry, this._pooledMaterials[index] ); group.add(body); return group; }, cleanupEchoes() { this.echoes.forEach(e => scene.remove(e.mesh)); this.echoes = []; }, record(position, rotation) { // v7.89: Use pooled vector instead of clone() - circular buffer pattern const pooledPos = this._positionPool[this._positionPoolIndex]; pooledPos.copy(position); this.timeline.push({ pos: pooledPos, rot: rotation }); this._positionPoolIndex = (this._positionPoolIndex + 1) % this.maxHistory; if (this.timeline.length > this.maxHistory) this.timeline.shift(); } }; function runTemporalAI(dt) { if (AI_BEHAVIOR.current !== 'temporal' || mode !== 'world' || !worldState.player) { if (TEMPORAL_AI.echoes.length > 0) TEMPORAL_AI.cleanupEchoes(); return false; } const player = worldState.player; // Record current position TEMPORAL_AI.record(player.position, player.rotation?.y || 0); // Update echo positions from timeline TEMPORAL_AI.echoes.forEach(echo => { const historyIndex = TEMPORAL_AI.timeline.length - echo.delay; if (historyIndex >= 0 && TEMPORAL_AI.timeline[historyIndex]) { const past = TEMPORAL_AI.timeline[historyIndex]; echo.mesh.position.copy(past.pos); echo.mesh.position.y += 1; echo.mesh.rotation.y = past.rot; } }); // v7.79: Auto-explore while recording - distanceToSquared optimization if (!worldState.target || player.position.distanceToSquared(worldState.target) < 9) { // 3*3=9 // v7.88: Use pooled exploration target vector if (!TEMPORAL_AI._exploreTarget) TEMPORAL_AI._exploreTarget = new THREE.Vector3(); TEMPORAL_AI._exploreTarget.set( (Math.random() - 0.5) * 60, player.position.y, (Math.random() - 0.5) * 60 ); worldState.target = TEMPORAL_AI._exploreTarget; } return true; } // 4. CHAOS AGENT - Does the OPPOSITE of optimal const CHAOS_AI = { state: 'chaos', lastDecisionTime: 0, chaosLevel: 0, // v7.88: Pre-allocated edge target vector _edgeTarget: null, // v7.89: Pre-allocated away direction vector for resource avoidance _awayDir: null, discoveries: [], init() { this.chaosLevel = 0; this.discoveries = []; } }; function runChaosAI(dt) { if (AI_BEHAVIOR.current !== 'chaos' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); if (now - CHAOS_AI.lastDecisionTime < 500) return true; CHAOS_AI.lastDecisionTime = now; // Find the "worst" decision and do it let choices = []; // v8.02: forEach to for loop conversion for hot path // Option 1: Run TOWARD enemies (opposite of safe) const chaosMobs = worldState.mobs; const chaosMobsLen = chaosMobs.length; for (let i = 0; i < chaosMobsLen; i++) { const mob = chaosMobs[i]; if (mob.parent) choices.push({ target: mob.position, chaos: 'running INTO danger!' }); } // Option 2: Run AWAY from resources (opposite of efficient) // v7.89: Use pooled vector for away direction calculation if (!CHAOS_AI._awayDir) CHAOS_AI._awayDir = new THREE.Vector3(); // v8.02: forEach to for loop conversion const chaosInteractables = worldState.interactables; const chaosInteractablesLen = chaosInteractables.length; for (let i = 0; i < chaosInteractablesLen; i++) { const obj = chaosInteractables[i]; if (obj.userData?.type === 'tree' || obj.userData?.type === 'rock') { CHAOS_AI._awayDir.copy(player.position).sub(obj.position).normalize().multiplyScalar(20).add(player.position); choices.push({ target: CHAOS_AI._awayDir, chaos: 'avoiding resources!' }); } } // Option 3: Go to random edge of map // v7.88: Use pooled edge target vector if (!CHAOS_AI._edgeTarget) CHAOS_AI._edgeTarget = new THREE.Vector3(); CHAOS_AI._edgeTarget.set((Math.random() > 0.5 ? 1 : -1) * 80, player.position.y, (Math.random() > 0.5 ? 1 : -1) * 80); choices.push({ target: CHAOS_AI._edgeTarget, chaos: 'exploring the void!' }); // Pick randomly from chaos options if (choices.length > 0) { const choice = choices[Math.floor(Math.random() * choices.length)]; // v7.86: Use setWorldTarget instead of clone() setWorldTarget(choice.target); CHAOS_AI.chaosLevel++; if (CHAOS_AI.chaosLevel % 10 === 0) { showNotification(`🎭 Chaos level ${CHAOS_AI.chaosLevel}: ${choice.chaos}`, 'warning'); } } return true; } // 5. PRECOGNITION - Predicts future events // v7.85: Added pre-allocated vectors to avoid per-frame/per-mob allocations // v7.86: Added geometry pooling to avoid RingGeometry allocation per marker const PRECOG_AI = { state: 'sensing', predictions: [], predictionMeshes: [], lastPredictionTime: 0, // v7.85: Pre-allocated vectors for hot path optimization _futurePos: new THREE.Vector3(), _toMarker: new THREE.Vector3(), _targetDir: new THREE.Vector3(), _safeZone: new THREE.Vector3(), // v7.86: Pooled geometry for prediction markers - reused across all markers _ringGeometry: null, // v7.86: Pooled materials by type to avoid per-marker material allocation _materials: {}, init() { this.cleanupPredictions(); // v7.86: Initialize pooled geometry and materials if (!this._ringGeometry) { this._ringGeometry = new THREE.RingGeometry(1, 1.5, 32); } if (!this._materials.danger) { this._materials.danger = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.5, side: THREE.DoubleSide }); this._materials.resource = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.5, side: THREE.DoubleSide }); this._materials.event = new THREE.MeshBasicMaterial({ color: 0xffff00, transparent: true, opacity: 0.5, side: THREE.DoubleSide }); this._materials.default = new THREE.MeshBasicMaterial({ color: 0xaa00ff, transparent: true, opacity: 0.5, side: THREE.DoubleSide }); } }, cleanupPredictions() { this.predictionMeshes.forEach(m => scene.remove(m)); this.predictionMeshes = []; this.predictions = []; }, // v7.86: Optimized to reuse pooled geometry and materials createPredictionMarker(position, type) { // Ensure geometry/materials are initialized if (!this._ringGeometry) this.init(); const material = this._materials[type] || this._materials.default; const ring = new THREE.Mesh(this._ringGeometry, material); ring.rotation.x = -Math.PI / 2; ring.position.copy(position); ring.position.y = 0.5; return ring; } }; // v7.85: Optimized to use pre-allocated vectors instead of cloning per mob/frame function runPrecogAI(dt) { if (AI_BEHAVIOR.current !== 'precog' || mode !== 'world' || !worldState.player) { if (PRECOG_AI.predictionMeshes.length > 0) PRECOG_AI.cleanupPredictions(); return false; } const player = worldState.player; const now = performance.now(); // Generate predictions every 3 seconds if (now - PRECOG_AI.lastPredictionTime > 3000) { PRECOG_AI.lastPredictionTime = now; PRECOG_AI.cleanupPredictions(); // v8.02: forEach to for loop conversion for hot path // Predict mob movements - v7.85: use pre-allocated _futurePos const precogMobs = worldState.mobs; const precogMobsLen = precogMobs.length; for (let i = 0; i < precogMobsLen; i++) { const mob = precogMobs[i]; if (!mob.parent) continue; PRECOG_AI._futurePos.copy(mob.position); if (mob.userData?.velocity) { // v7.85: Avoid velocity.clone() - scale in place then add PRECOG_AI._futurePos.x += mob.userData.velocity.x * 30; PRECOG_AI._futurePos.y += mob.userData.velocity.y * 30; PRECOG_AI._futurePos.z += mob.userData.velocity.z * 30; } const marker = PRECOG_AI.createPredictionMarker(PRECOG_AI._futurePos, 'danger'); PRECOG_AI.predictionMeshes.push(marker); scene.add(marker); } // Predict safe zones - v7.85: use pre-allocated _safeZone if (SHIP_STATE.mesh?.position) { PRECOG_AI._safeZone.copy(SHIP_STATE.mesh.position); } else { PRECOG_AI._safeZone.set(0, 0, 0); } const safeMarker = PRECOG_AI.createPredictionMarker(PRECOG_AI._safeZone, 'resource'); PRECOG_AI.predictionMeshes.push(safeMarker); scene.add(safeMarker); } // v8.02: forEach to for loop conversion for hot path // Animate prediction markers const predMeshes = PRECOG_AI.predictionMeshes; const predMeshesLen = predMeshes.length; for (let i = 0; i < predMeshesLen; i++) { const m = predMeshes[i]; m.rotation.z += 0.02; m.material.opacity = 0.3 + Math.sin(now * 0.005 + i) * 0.2; } // Move toward predicted resources, away from predicted danger // v7.85: Use pre-allocated _targetDir instead of new Vector3 // v8.12: Use lengthSq for distance checks to avoid unnecessary sqrt calls PRECOG_AI._targetDir.set(0, 0, 0); for (let i = 0; i < predMeshesLen; i++) { const m = predMeshes[i]; // v7.85: Use pre-allocated _toMarker instead of clone PRECOG_AI._toMarker.copy(m.position).sub(player.position); const distSq = PRECOG_AI._toMarker.lengthSq(); if (m.material.color.getHex() === 0xff0000 && distSq < 400) { // 20*20 = 400 PRECOG_AI._targetDir.sub(PRECOG_AI._toMarker.normalize()); } else if (m.material.color.getHex() === 0x00ff00) { PRECOG_AI._targetDir.add(PRECOG_AI._toMarker.normalize().multiplyScalar(0.5)); } } if (PRECOG_AI._targetDir.lengthSq() > 0.01) { // v8.12: 0.1*0.1 = 0.01 // v7.86: Use setWorldTargetWithOffset instead of clone().add() setWorldTargetWithOffset(player.position, PRECOG_AI._targetDir.normalize().multiplyScalar(10)); } return true; } // 6. FLUID DYNAMICS - Water-like movement const FLUID_AI = { state: 'flowing', velocity: new THREE.Vector3(), viscosity: 0.95, flowField: [], particleTrail: [], // v7.84: Pre-allocated vectors for runFluidAI hot path _flowDir: new THREE.Vector3(), _diff: new THREE.Vector3(), _randomFlow: new THREE.Vector3(), _tempTarget: new THREE.Vector3(), init() { this.velocity.set(0, 0, 0); this.particleTrail = []; } }; // v7.84: Optimized to use pre-allocated vectors instead of creating 4-6 new Vector3 per frame function runFluidAI(dt) { if (AI_BEHAVIOR.current !== 'fluid' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; // Calculate flow direction based on "terrain gradient" FLUID_AI._flowDir.set(0, 0, 0); // v8.02: forEach to for loop conversion for hot path // v8.12: Use lengthSq for initial distance check to avoid sqrt when outside range // Avoid obstacles (rocks, trees) const fluidInteractables = worldState.interactables; const fluidInteractablesLen = fluidInteractables.length; for (let i = 0; i < fluidInteractablesLen; i++) { const obj = fluidInteractables[i]; if (!obj.parent) continue; FLUID_AI._diff.copy(player.position).sub(obj.position); const distSq = FLUID_AI._diff.lengthSq(); if (distSq < 64) { // 8*8 = 64 (squared threshold) const dist = Math.sqrt(distSq); // v8.12: Only compute sqrt when needed FLUID_AI._flowDir.add(FLUID_AI._diff.divideScalar(dist).multiplyScalar(8 / dist)); } } // v8.02: forEach to for loop conversion // v8.12: Use lengthSq for initial distance check // Avoid mobs const fluidMobs = worldState.mobs; const fluidMobsLen = fluidMobs.length; for (let i = 0; i < fluidMobsLen; i++) { const mob = fluidMobs[i]; if (!mob.parent) continue; FLUID_AI._diff.copy(player.position).sub(mob.position); const distSq = FLUID_AI._diff.lengthSq(); if (distSq < 144) { // 12*12 = 144 (squared threshold) const dist = Math.sqrt(distSq); // v8.12: Only compute sqrt when needed FLUID_AI._flowDir.add(FLUID_AI._diff.divideScalar(dist).multiplyScalar(12 / dist)); } } // Add gentle random flow FLUID_AI._randomFlow.set( Math.sin(performance.now() * 0.001) * 0.5, 0, Math.cos(performance.now() * 0.0007) * 0.5 ); FLUID_AI._flowDir.add(FLUID_AI._randomFlow); // Apply viscosity FLUID_AI.velocity.multiplyScalar(FLUID_AI.viscosity); FLUID_AI.velocity.add(FLUID_AI._flowDir.multiplyScalar(0.1)); FLUID_AI.velocity.clampLength(0, 2); // v7.84: Use pre-allocated temp vector for target calculation // v7.86: Use setWorldTargetWithOffset instead of clone().add() FLUID_AI._tempTarget.copy(FLUID_AI.velocity).multiplyScalar(5); setWorldTargetWithOffset(player.position, FLUID_AI._tempTarget); return true; } // 7. JESTER PROTOCOL - Prioritizes "interesting" over "efficient" // v7.87: Added pre-allocated vector to avoid 4 Vector3 allocations per action const JESTER_AI = { state: 'performing', lastJokeTime: 0, artPieces: [], funFactor: 0, // v7.87: Pre-allocated vector for spinTarget to avoid per-action allocation _spinTarget: null, init() { this.funFactor = 0; this.artPieces = []; // v7.87: Initialize pre-allocated vector if (!this._spinTarget) { this._spinTarget = new THREE.Vector3(); } } }; function runJesterAI(dt) { if (AI_BEHAVIOR.current !== 'jester' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); // Do something "interesting" every 2 seconds if (now - JESTER_AI.lastJokeTime > 2000) { JESTER_AI.lastJokeTime = now; JESTER_AI.funFactor++; // v7.87: Ensure pre-allocated vector exists if (!JESTER_AI._spinTarget) JESTER_AI._spinTarget = new THREE.Vector3(); const actions = [ () => { // Spin in circles // v7.87: Reuse pre-allocated _spinTarget instead of new Vector3 JESTER_AI._spinTarget.set( player.position.x + Math.sin(now * 0.01) * 5, player.position.y, player.position.z + Math.cos(now * 0.01) * 5 ); JESTER_AI.spinTarget = JESTER_AI._spinTarget; showNotification('🎪 Wheeeee!', 'info'); }, () => { // Jump toward nearest tree const tree = worldState.interactables.find(o => o.userData?.type === 'tree'); if (tree) { // v7.87: Copy into pre-allocated vector instead of clone() JESTER_AI._spinTarget.copy(tree.position); JESTER_AI.spinTarget = JESTER_AI._spinTarget; showNotification('🎪 Tree friend!', 'info'); } }, () => { // Run in a zigzag // v7.87: Use set() and addScaledVector pattern instead of new + add JESTER_AI._spinTarget.copy(player.position); JESTER_AI._spinTarget.x += (Math.random() > 0.5 ? 15 : -15); JESTER_AI._spinTarget.z += (Math.random() > 0.5 ? 15 : -15); JESTER_AI.spinTarget = JESTER_AI._spinTarget; showNotification('🎪 Ziggy zaggy!', 'info'); }, () => { // "Art installation" - just stand still dramatically // v7.87: Copy into pre-allocated vector instead of clone() JESTER_AI._spinTarget.copy(player.position); JESTER_AI.spinTarget = JESTER_AI._spinTarget; showNotification('🎪 *strikes pose*', 'info'); } ]; actions[Math.floor(Math.random() * actions.length)](); } if (JESTER_AI.spinTarget) { worldState.target = JESTER_AI.spinTarget; } return true; } // 8. LIGHTNING ROUTER - Optimal path through ALL objectives // v7.87: Added pooled material and pre-allocated vectors for path calculation // v7.88: Added pooled vector array for objectives to avoid per-recalc allocations const LIGHTNING_AI = { state: 'calculating', waypoints: [], currentWaypoint: 0, pathMesh: null, lastCalculation: 0, // v7.87: Pooled material for path line to avoid per-recalculation allocation _pathMaterial: null, // v7.87: Pre-allocated vector for current position in TSP _currentPos: null, // v7.88: Pooled vectors for objectives array (max 100 objectives) _objectivePool: null, _objectivePoolSize: 100, // v7.88: Pre-allocated vector for path drawing start point _pathStartPos: null, init() { this.waypoints = []; this.currentWaypoint = 0; if (this.pathMesh) scene.remove(this.pathMesh); // v7.87: Initialize pooled material if (!this._pathMaterial) { this._pathMaterial = new THREE.LineBasicMaterial({ color: 0xffff88, transparent: true, opacity: 0.6 }); } if (!this._currentPos) { this._currentPos = new THREE.Vector3(); } // v7.88: Initialize pooled vectors for objectives if (!this._objectivePool) { this._objectivePool = []; for (let i = 0; i < this._objectivePoolSize; i++) { this._objectivePool.push(new THREE.Vector3()); } } if (!this._pathStartPos) { this._pathStartPos = new THREE.Vector3(); } } }; function runLightningAI(dt) { if (AI_BEHAVIOR.current !== 'lightning' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); // Recalculate path every 5 seconds if (now - LIGHTNING_AI.lastCalculation > 5000 || LIGHTNING_AI.waypoints.length === 0) { LIGHTNING_AI.lastCalculation = now; LIGHTNING_AI.waypoints = []; // v7.87: Ensure pooled resources are initialized if (!LIGHTNING_AI._currentPos) LIGHTNING_AI._currentPos = new THREE.Vector3(); if (!LIGHTNING_AI._pathMaterial) { LIGHTNING_AI._pathMaterial = new THREE.LineBasicMaterial({ color: 0xffff88, transparent: true, opacity: 0.6 }); } // v7.88: Ensure objective pool is initialized if (!LIGHTNING_AI._objectivePool) { LIGHTNING_AI._objectivePool = []; for (let i = 0; i < LIGHTNING_AI._objectivePoolSize; i++) { LIGHTNING_AI._objectivePool.push(new THREE.Vector3()); } } if (!LIGHTNING_AI._pathStartPos) { LIGHTNING_AI._pathStartPos = new THREE.Vector3(); } // v7.88: Collect all objectives using pooled vectors instead of clone() // v8.09: forEach to for loop const objectives = []; let poolIdx = 0; const interactables = worldState.interactables; for (let oi = 0, olen = interactables.length; oi < olen; oi++) { const obj = interactables[oi]; if (obj.parent && (obj.userData?.type === 'tree' || obj.userData?.type === 'rock')) { if (poolIdx < LIGHTNING_AI._objectivePoolSize) { LIGHTNING_AI._objectivePool[poolIdx].copy(obj.position); objectives.push(LIGHTNING_AI._objectivePool[poolIdx]); poolIdx++; } } } // Simple nearest-neighbor TSP solution // v7.87: Use pre-allocated _currentPos instead of clone() // v8.0: Use distanceToSquared for comparison (order preserved, avoids sqrt) LIGHTNING_AI._currentPos.copy(player.position); while (objectives.length > 0) { let nearest = 0; let nearestDistSq = Infinity; for (let i = 0; i < objectives.length; i++) { const distSq = LIGHTNING_AI._currentPos.distanceToSquared(objectives[i]); if (distSq < nearestDistSq) { nearestDistSq = distSq; nearest = i; } } LIGHTNING_AI.waypoints.push(objectives[nearest]); LIGHTNING_AI._currentPos.copy(objectives.splice(nearest, 1)[0]); } LIGHTNING_AI.currentWaypoint = 0; // Draw path if (LIGHTNING_AI.pathMesh) scene.remove(LIGHTNING_AI.pathMesh); if (LIGHTNING_AI.waypoints.length > 1) { // v7.88: Use pooled _pathStartPos instead of clone() LIGHTNING_AI._pathStartPos.copy(player.position); const points = [LIGHTNING_AI._pathStartPos, ...LIGHTNING_AI.waypoints]; const geometry = new THREE.BufferGeometry().setFromPoints(points); // v7.87: Use pooled _pathMaterial instead of new material per recalculation LIGHTNING_AI.pathMesh = new THREE.Line(geometry, LIGHTNING_AI._pathMaterial); LIGHTNING_AI.pathMesh.position.y = 1; scene.add(LIGHTNING_AI.pathMesh); } showNotification(`⚡ Path calculated: ${LIGHTNING_AI.waypoints.length} objectives`, 'success'); } // Follow waypoints // v7.80: distanceToSquared optimization if (LIGHTNING_AI.waypoints.length > 0) { const target = LIGHTNING_AI.waypoints[LIGHTNING_AI.currentWaypoint]; if (target) { // v7.86: Use setWorldTarget instead of clone() setWorldTarget(target); if (player.position.distanceToSquared(target) < 9) { // 3*3=9 LIGHTNING_AI.currentWaypoint++; if (LIGHTNING_AI.currentWaypoint >= LIGHTNING_AI.waypoints.length) { LIGHTNING_AI.currentWaypoint = 0; LIGHTNING_AI.lastCalculation = 0; // Force recalculation } } } } return true; } // 9. SHADOW STALKER - Moves only when unobserved // v7.87: Added pre-allocated vectors to avoid per-frame/per-mob allocations const SHADOW_AI = { state: 'hiding', isObserved: false, lastMoveTime: 0, targetShadow: null, stealthMeter: 100, // v7.87: Pre-allocated vectors for candidate spots (5 candidates checked per frame) _candidates: null, // v7.87: Pre-allocated vector for toPlayer direction check _toPlayer: null, // v7.87: Best spot result vector _bestSpot: null, init() { this.stealthMeter = 100; this.isObserved = false; // v7.87: Initialize pre-allocated vectors if (!this._candidates) { this._candidates = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ]; } if (!this._toPlayer) { this._toPlayer = new THREE.Vector3(); } if (!this._bestSpot) { this._bestSpot = new THREE.Vector3(); } } }; function runShadowAI(dt) { if (AI_BEHAVIOR.current !== 'shadow' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); // v7.87: Ensure pre-allocated vectors exist if (!SHADOW_AI._toPlayer) SHADOW_AI.init(); // v8.02: forEach to for loop conversion for hot path // v7.79: Check if any mob is "looking" at player - distanceToSquared optimization SHADOW_AI.isObserved = false; const observeRangeSq = 625; // 25 * 25 const shadowMobs = worldState.mobs; const shadowMobsLen = shadowMobs.length; for (let i = 0; i < shadowMobsLen; i++) { const mob = shadowMobs[i]; if (!mob.parent) continue; const distSq = player.position.distanceToSquared(mob.position); if (distSq < observeRangeSq) { // Check if mob is facing player (simplified) // v7.87: Use pre-allocated _toPlayer instead of clone() SHADOW_AI._toPlayer.copy(player.position).sub(mob.position).normalize(); SHADOW_AI.isObserved = true; } } // Update state if (SHADOW_AI.isObserved) { SHADOW_AI.state = 'frozen'; SHADOW_AI.stealthMeter = Math.max(0, SHADOW_AI.stealthMeter - dt * 10); worldState.target = null; // FREEZE! } else { SHADOW_AI.state = 'stalking'; SHADOW_AI.stealthMeter = Math.min(100, SHADOW_AI.stealthMeter + dt * 5); // Find darkest spot (furthest from mobs) // v7.87: Use pre-allocated candidate vectors and distanceToSquared let bestIdx = -1; let bestScore = -Infinity; for (let i = 0; i < 5; i++) { const candidate = SHADOW_AI._candidates[i]; candidate.set( player.position.x + (Math.random() - 0.5) * 30, player.position.y, player.position.z + (Math.random() - 0.5) * 30 ); let score = 0; // v8.02: forEach to for loop conversion // v7.87: Use distanceToSquared for relative comparison (avoids sqrt) for (let j = 0; j < shadowMobsLen; j++) { const mob = shadowMobs[j]; if (mob.parent) score += candidate.distanceToSquared(mob.position); } if (score > bestScore) { bestScore = score; bestIdx = i; } } if (bestIdx >= 0) { // v7.87: Copy best candidate to _bestSpot for stable reference SHADOW_AI._bestSpot.copy(SHADOW_AI._candidates[bestIdx]); worldState.target = SHADOW_AI._bestSpot; } } return true; } // 10. RHYTHMIC CONDUCTOR - Actions sync to internal beat // v7.87: Added geometry and material pooling to avoid allocations per beat const RHYTHM_AI = { state: 'conducting', bpm: 120, beatPhase: 0, lastBeatTime: 0, beatCount: 0, notes: [], // v7.87: Pooled geometry and material for beat pulse visuals _pulseGeometry: null, _pulseMaterial: null, // v7.87: Pre-allocated vector for target position _targetVec: null, init() { this.beatPhase = 0; this.beatCount = 0; this.notes = []; this.lastBeatTime = performance.now(); // v7.87: Initialize pooled geometry and material if (!this._pulseGeometry) { this._pulseGeometry = new THREE.RingGeometry(0.5, 1, 16); } if (!this._pulseMaterial) { this._pulseMaterial = new THREE.MeshBasicMaterial({ color: 0xff88ff, transparent: true, opacity: 0.8, side: THREE.DoubleSide }); } if (!this._targetVec) { this._targetVec = new THREE.Vector3(); } }, onBeat() { this.beatCount++; // Create visual beat pulse if (worldState.player) { // v7.87: Ensure pooled resources are initialized if (!this._pulseGeometry) this.init(); // v7.87: Clone material for independent opacity animation per pulse const pulseMat = this._pulseMaterial.clone(); const pulse = new THREE.Mesh(this._pulseGeometry, pulseMat); pulse.position.copy(worldState.player.position); pulse.position.y = 0.1; pulse.rotation.x = -Math.PI / 2; pulse.userData.birthTime = performance.now(); this.notes.push(pulse); scene.add(pulse); // Play a tone try { // v7.28: Use shared AudioContext if (!RHYTHM_AI.audioCtx) { RHYTHM_AI.audioCtx = getSharedAudioContext(); } if (RHYTHM_AI.audioCtx) { const osc = RHYTHM_AI.audioCtx.createOscillator(); const gain = RHYTHM_AI.audioCtx.createGain(); osc.connect(gain); gain.connect(RHYTHM_AI.audioCtx.destination); const notes = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00]; osc.frequency.value = notes[this.beatCount % notes.length]; gain.gain.setValueAtTime(0.1, RHYTHM_AI.audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, RHYTHM_AI.audioCtx.currentTime + 0.2); osc.start(); osc.stop(RHYTHM_AI.audioCtx.currentTime + 0.2); } } catch (e) { /* Audio not available */ } } } }; function runRhythmAI(dt) { if (AI_BEHAVIOR.current !== 'rhythm' || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); const beatInterval = 60000 / RHYTHM_AI.bpm; if (now - RHYTHM_AI.lastBeatTime >= beatInterval) { RHYTHM_AI.lastBeatTime = now; RHYTHM_AI.onBeat(); // Move on beat // v7.87: Use pre-allocated _targetVec instead of new Vector3 const angle = (RHYTHM_AI.beatCount * 0.5) % (Math.PI * 2); if (!RHYTHM_AI._targetVec) RHYTHM_AI._targetVec = new THREE.Vector3(); RHYTHM_AI._targetVec.set( player.position.x + Math.sin(angle) * 8, player.position.y, player.position.z + Math.cos(angle) * 8 ); worldState.target = RHYTHM_AI._targetVec; } // Update visual notes RHYTHM_AI.notes = RHYTHM_AI.notes.filter(note => { const age = (now - note.userData.birthTime) / 1000; if (age > 1) { scene.remove(note); return false; } note.scale.setScalar(1 + age * 3); note.material.opacity = 0.8 - age * 0.8; return true; }); return true; } // Master AI update function - routes to correct behavior function updateAIBehavior(dt) { if (mode !== 'world') return false; // v9.4: Run colony planner in background (independent of current AI behavior) if (typeof runColonyPlanner === 'function') { runColonyPlanner(); } switch (AI_BEHAVIOR.current) { case 'manual': return false; case 'explorer': return runAutoExplore(dt); case 'pusher': return runLanePushAI(dt); case 'miner': return runMinerAI(dt); case 'defender': return runDefenderAI(dt); case 'terraformer': return runTerraformerAI(dt); case 'builder': return runBuilderAI(dt); case 'hunter': return runHunterAI(dt); case 'trader': return runTraderAI(dt); // v7.3: ADVANCED AI BEHAVIORS case 'evolutionary': return runEvolutionaryAI(dt); case 'hivemind': return runHivemindAI(dt); case 'temporal': return runTemporalAI(dt); case 'chaos': return runChaosAI(dt); case 'precog': return runPrecogAI(dt); case 'fluid': return runFluidAI(dt); case 'jester': return runJesterAI(dt); case 'lightning': return runLightningAI(dt); case 'shadow': return runShadowAI(dt); case 'rhythm': return runRhythmAI(dt); default: return false; } } // ============================================ // v6.68: AUTONOMOUS LANE PUSH AI SYSTEM // Actively pushes lanes, uses abilities, takes towers // ============================================ const LANE_PUSH_AI = { enabled: false, state: 'idle', // idle, pushing, fighting, retreating, sieging currentLane: null, // 'top', 'mid', 'bot' targetTower: null, lastSiegedTier: null, // Track which tower tier for announcements (T1, T2, T3) lastDecisionTime: 0, decisionInterval: 500, // Decide every 500ms lastAbilityTime: 0, abilityInterval: 300, // Check abilities every 300ms retreatThreshold: 0.25, // Retreat at 25% HP aggressiveThreshold: 0.6, // Be aggressive above 60% HP waveFollowDistance: 8, // Stay this close to friendly wave towerSiegeRange: 15, // Attack tower from this range stats: { creepsKilled: 0, towersDestroyed: 0, abilitiesUsed: 0, pushTime: 0 }, // v7.86: Pre-allocated vector for siege state direction calculations _tempDir: new THREE.Vector3(), // v8.18: Pre-allocated vectors for frequent calculations _enemyBasePos: new THREE.Vector3(0, 0, 45), _pushDir: new THREE.Vector3(), _laneTop: new THREE.Vector3(-30, 0, 0), _laneMid: new THREE.Vector3(0, 0, 0), _laneBot: new THREE.Vector3(30, 0, 0), // v8.22: Pre-allocated vector for frontline position (avoids clone() in getLanePushData) _frontlinePos: new THREE.Vector3() }; function toggleLanePushAI() { LANE_PUSH_AI.enabled = !LANE_PUSH_AI.enabled; if (LANE_PUSH_AI.enabled) { LANE_PUSH_AI.state = 'idle'; LANE_PUSH_AI.currentLane = null; autoExplore.enabled = false; // Disable regular autopilot updateAutoExploreUI(); showNotification('🤖 AUTONOMOUS++ ENGAGED - Strategic lane domination!', 'success'); addCopilotMessage('🤖 AUTONOMOUS++ activated! I will strategically push lanes, use abilities to clear waves, and siege towers in order (T1→T2→T3). Watch the AI dominate!', 'ai'); } else { LANE_PUSH_AI.state = 'idle'; showNotification('🤖 AUTONOMOUS++ DISENGAGED', 'info'); } updateLanePushUI(); } function updateLanePushUI() { const btn = document.getElementById('lane-push-btn'); const indicator = document.getElementById('auto-explore-indicator'); if (btn) { btn.textContent = LANE_PUSH_AI.enabled ? '🛑 AUTONOMOUS++ OFF' : '🤖 Autonomous++'; btn.style.background = LANE_PUSH_AI.enabled ? '#ff4444' : '#4488ff'; } if (indicator && LANE_PUSH_AI.enabled) { const laneName = LANE_PUSH_AI.currentLane?.toUpperCase() || 'LANE'; const state = LANE_PUSH_AI.state; let statusText = `🤖 A++ ${laneName}`; // Show strategic state info if (state === 'sieging' && LANE_PUSH_AI.lastSiegedTier) { statusText = `⚔️ SIEGE ${laneName} ${LANE_PUSH_AI.lastSiegedTier}`; indicator.style.color = '#ff4400'; } else if (state === 'fighting') { statusText = `⚔️ FIGHTING ${laneName}`; indicator.style.color = '#ff8800'; } else if (state === 'retreating') { statusText = `🏃 RETREATING`; indicator.style.color = '#ff0000'; } else { statusText = `🎯 PUSHING ${laneName}`; indicator.style.color = '#00ff88'; } indicator.textContent = statusText; } } // Main AI update function function runLanePushAI(dt) { if (!LANE_PUSH_AI.enabled || mode !== 'world' || !worldState.player) return false; const player = worldState.player; const now = performance.now(); // v8.26: Guard against undefined gameData.player if (!gameData?.player?.hp || !gameData?.player?.maxHp) return false; const playerHpPercent = gameData.player.hp / gameData.player.maxHp; // Track push time LANE_PUSH_AI.stats.pushTime += dt; // === ABILITY AI: Use abilities to clear waves === if (now - LANE_PUSH_AI.lastAbilityTime > LANE_PUSH_AI.abilityInterval) { LANE_PUSH_AI.lastAbilityTime = now; runAbilityAI(player, playerHpPercent); } // === DECISION AI: Runs less frequently === if (now - LANE_PUSH_AI.lastDecisionTime < LANE_PUSH_AI.decisionInterval) { return true; } LANE_PUSH_AI.lastDecisionTime = now; // === RETREAT CHECK === if (playerHpPercent < LANE_PUSH_AI.retreatThreshold) { LANE_PUSH_AI.state = 'retreating'; retreatToShip(player); return true; } // === CHOOSE LANE if none selected === if (!LANE_PUSH_AI.currentLane) { LANE_PUSH_AI.currentLane = chooseBestLane(); LANE_PUSH_AI.state = 'pushing'; updateLanePushUI(); } // === GET LANE DATA === const laneData = getLanePushData(LANE_PUSH_AI.currentLane); // === STATE MACHINE === switch (LANE_PUSH_AI.state) { case 'pushing': executePushState(player, laneData, playerHpPercent); break; case 'fighting': executeFightState(player, laneData); break; case 'sieging': executeSiegeState(player, laneData); break; case 'retreating': if (playerHpPercent > LANE_PUSH_AI.aggressiveThreshold) { LANE_PUSH_AI.state = 'pushing'; } else { retreatToShip(player); } break; } return true; } // Choose the best lane to push function chooseBestLane() { const lanes = ['top', 'mid', 'bot']; let bestLane = 'mid'; let bestScore = -Infinity; lanes.forEach(laneKey => { const data = getLanePushData(laneKey); // Score based on: friendly creep advantage, fewer enemy towers, closer to enemy base let score = 0; score += (data.friendlyCreeps - data.enemyCreeps) * 10; score -= data.enemyTowers * 50; score += data.friendlyTowers * 30; score += (50 - data.distanceToEnemyBase) * 2; if (score > bestScore) { bestScore = score; bestLane = laneKey; } }); return bestLane; } // Get lane-specific data for AI decisions // v6.68: Strategic tower targeting - must destroy T1 before T2, T2 before T3 function getLanePushData(laneKey) { const data = { friendlyCreeps: 0, enemyCreeps: 0, friendlyTowers: 0, enemyTowers: 0, nearestEnemyCreep: null, nearestEnemyTower: null, // The FRONTMOST tower we should attack nextEnemyTower: null, // The tower after the frontmost nearestFriendlyCreep: null, frontlinePosition: null, distanceToEnemyBase: 100, // v6.68: Tower tier tracking enemyTowerTiers: [], // All enemy towers sorted by tier currentTowerTier: 0, // Which tier we're attacking (1, 2, or 3) towersDestroyed: 0 // How many enemy towers destroyed in this lane }; if (!creepWaveState.creeps) return data; // Count creeps in this lane // v7.74: Use distanceToSquared for performance (avoids sqrt) // v8.01: forEach to for loop conversion const playerPos = worldState.player?.position; for (let i = 0, len = creepWaveState.creeps.length; i < len; i++) { const creep = creepWaveState.creeps[i]; if (!creep || !creep.userData || creep.userData.laneKey !== laneKey) continue; if (creep.userData.team === 'A') { data.friendlyCreeps++; if (!data.nearestFriendlyCreep || (playerPos && creep.position.distanceToSquared(playerPos) < data.nearestFriendlyCreep.position.distanceToSquared(playerPos))) { data.nearestFriendlyCreep = creep; } } else { data.enemyCreeps++; if (!data.nearestEnemyCreep || (playerPos && creep.position.distanceToSquared(playerPos) < data.nearestEnemyCreep.position.distanceToSquared(playerPos))) { data.nearestEnemyCreep = creep; } } } // v6.68: Strategic tower analysis - find towers in correct attack order // For hostile towers: segment 3 = T1 (frontline), segment 4 = T2, segment 5 = T3 (base) // We MUST destroy T1 before we can attack T2, etc. // v8.01: forEach to for loop conversion if (laneSupportState.laneTowers) { const enemyTowersInLane = []; const friendlyTowersInLane = []; for (let i = 0, len = laneSupportState.laneTowers.length; i < len; i++) { const tower = laneSupportState.laneTowers[i]; if (!tower || !tower.active || tower.laneKey !== laneKey) continue; if (tower.team === 'robot') { data.friendlyTowers++; friendlyTowersInLane.push(tower); } else { data.enemyTowers++; enemyTowersInLane.push(tower); } } // Sort enemy towers by segment (lowest segment = frontmost = attack first) // Hostile towers are at segments 3, 4, 5 - so segment 3 is T1 enemyTowersInLane.sort((a, b) => a.segment - b.segment); data.enemyTowerTiers = enemyTowersInLane; // The frontmost tower is the one we should attack (lowest segment) if (enemyTowersInLane.length > 0) { data.nearestEnemyTower = enemyTowersInLane[0]; // T1 or next available data.currentTowerTier = 4 - enemyTowersInLane[0].segment; // segment 3=T1, 4=T2, 5=T3 if (enemyTowersInLane.length > 1) { data.nextEnemyTower = enemyTowersInLane[1]; // Preview next target } } // Track how many towers we've destroyed (3 - remaining) data.towersDestroyed = 3 - enemyTowersInLane.length; } // Calculate frontline (average position of friendly creeps) // v8.22: Use pooled vector instead of clone() if (data.nearestFriendlyCreep) { data.frontlinePosition = LANE_PUSH_AI._frontlinePos.copy(data.nearestFriendlyCreep.position); } // Calculate distance to enemy base based on remaining towers if (data.nearestEnemyTower) { data.distanceToEnemyBase = data.nearestEnemyTower.segment * 15; // Rough estimate } else { data.distanceToEnemyBase = 10; // No towers left, close to base! } return data; } // Execute PUSH state - follow friendly wave // v7.74: Use distanceToSquared for performance function executePushState(player, laneData, playerHpPercent) { // If enemies nearby, switch to fighting if (laneData.nearestEnemyCreep) { const distSqToEnemy = player.position.distanceToSquared(laneData.nearestEnemyCreep.position); if (distSqToEnemy < 144) { // 12 * 12 = 144 LANE_PUSH_AI.state = 'fighting'; return; } } // If no enemy towers left and we have advantage, we could siege base if (laneData.enemyTowers === 0 && laneData.friendlyCreeps > 0) { // Push toward enemy base area // v8.18: Use pre-allocated vector worldState.target = LANE_PUSH_AI._enemyBasePos; return; } // If enemy tower in range and we have creeps for cover, siege it // Strategic: Always target the frontmost tower (T1 → T2 → T3) if (laneData.nearestEnemyTower && laneData.friendlyCreeps >= 2) { const distSqToTower = player.position.distanceToSquared(laneData.nearestEnemyTower.position); if (distSqToTower < 625) { // 25 * 25 = 625 LANE_PUSH_AI.targetTower = laneData.nearestEnemyTower; LANE_PUSH_AI.lastSiegedTier = `T${laneData.currentTowerTier || 1}`; LANE_PUSH_AI.state = 'sieging'; const tierName = LANE_PUSH_AI.lastSiegedTier; const laneName = LANE_PUSH_AI.currentLane?.toUpperCase() || 'LANE'; showNotification(`⚔️ SIEGING ${laneName} ${tierName} TOWER`, 'warning'); return; } } // Follow friendly creeps / frontline if (laneData.frontlinePosition) { const distSqToFront = player.position.distanceToSquared(laneData.frontlinePosition); const followDistSq = LANE_PUSH_AI.waveFollowDistance * LANE_PUSH_AI.waveFollowDistance; if (distSqToFront > followDistSq) { // v7.86: Use setWorldTarget instead of clone() setWorldTarget(laneData.frontlinePosition); } else { // Stay with wave, move forward slightly // v8.18: Use pre-allocated vector LANE_PUSH_AI._pushDir.set(0, 0, 1); // Push toward enemy base // v7.86: Use setWorldTargetWithOffset instead of clone().add() setWorldTargetWithOffset(player.position, LANE_PUSH_AI._pushDir.multiplyScalar(5)); } } else { // No creeps, move to lane // v8.18: Use pre-allocated lane position vectors const lanePositions = { top: LANE_PUSH_AI._laneTop, mid: LANE_PUSH_AI._laneMid, bot: LANE_PUSH_AI._laneBot }; worldState.target = lanePositions[LANE_PUSH_AI.currentLane]; } } // Execute FIGHT state - attack enemy creeps // v7.74: Use distanceToSquared for performance function executeFightState(player, laneData) { if (!laneData.nearestEnemyCreep) { LANE_PUSH_AI.state = 'pushing'; return; } const enemy = laneData.nearestEnemyCreep; const distSqToEnemy = player.position.distanceToSquared(enemy.position); const interactionRangeSq = CONFIG.INTERACTION_RANGE * CONFIG.INTERACTION_RANGE; if (distSqToEnemy > interactionRangeSq) { // Move toward enemy // v7.86: Use setWorldTarget instead of clone() setWorldTarget(enemy.position); worldState.interactTarget = enemy; } else { // Attack! worldState.target = null; const now = performance.now(); if (now - worldState.lastActionTime > CONFIG.INTERACTION_COOLDOWN) { performAction(enemy); worldState.lastActionTime = now; LANE_PUSH_AI.stats.creepsKilled++; } } // Check if enemy wave cleared if (laneData.enemyCreeps === 0) { LANE_PUSH_AI.state = 'pushing'; } } // Execute SIEGE state - attack enemy tower (strategic: T1 → T2 → T3) // v7.74: Use distanceToSquared for performance function executeSiegeState(player, laneData) { const tower = LANE_PUSH_AI.targetTower; if (!tower || !tower.active) { // Tower destroyed! Announce it const tierName = LANE_PUSH_AI.lastSiegedTier || 'T1'; const laneName = LANE_PUSH_AI.currentLane?.toUpperCase() || 'LANE'; showNotification(`🏰 ${laneName} ${tierName} TOWER DESTROYED!`, 'success'); addCopilotMessage(`🤖 AUTONOMOUS++: Enemy ${tierName} tower in ${laneName} lane destroyed! ${laneData.enemyTowers > 0 ? 'Moving to next tower.' : 'All towers down - pushing to base!'}`, 'ai'); LANE_PUSH_AI.targetTower = null; LANE_PUSH_AI.state = 'pushing'; LANE_PUSH_AI.stats.towersDestroyed++; return; } // Track which tier we're sieging for announcements LANE_PUSH_AI.lastSiegedTier = `T${laneData.currentTowerTier || 1}`; const distSqToTower = player.position.distanceToSquared(tower.position); const distToTower = Math.sqrt(distSqToTower); // Need actual distance for range math // Keep distance from tower (let creeps tank) const idealRange = LANE_PUSH_AI.towerSiegeRange; if (distToTower > idealRange + 2) { // Move closer // v7.86: Use pre-allocated _tempDir and setWorldTargetWithOffset LANE_PUSH_AI._tempDir.copy(tower.position).sub(player.position).normalize().multiplyScalar(5); setWorldTargetWithOffset(player.position, LANE_PUSH_AI._tempDir); } else if (distToTower < idealRange - 2) { // Move back a bit // v7.86: Use pre-allocated _tempDir and setWorldTargetWithOffset LANE_PUSH_AI._tempDir.copy(player.position).sub(tower.position).normalize().multiplyScalar(3); setWorldTargetWithOffset(player.position, LANE_PUSH_AI._tempDir); } else { // In range - attack tower worldState.target = null; worldState.interactTarget = tower.mesh; const now = performance.now(); if (now - worldState.lastActionTime > CONFIG.INTERACTION_COOLDOWN) { performAction(tower.mesh); worldState.lastActionTime = now; } } // If no friendly creeps, retreat (tower will kill us) if (laneData.friendlyCreeps === 0) { LANE_PUSH_AI.state = 'retreating'; } } // Retreat to ship for healing // v7.74: Use distanceToSquared for performance function retreatToShip(player) { if (!SHIP_STATE.mesh) return; const shipPos = SHIP_STATE.mesh.position; const distSqToShip = player.position.distanceToSquared(shipPos); const healRangeMinusFive = SHIP_STATE.healing.range - 5; const healRangeSq = healRangeMinusFive * healRangeMinusFive; if (distSqToShip > healRangeSq) { // v7.86: Use setWorldTarget instead of clone() setWorldTarget(shipPos); } else { // At ship, just wait for healing worldState.target = null; } } // AI for using abilities efficiently // v7.74: Use distanceToSquared for performance function runAbilityAI(player, playerHpPercent) { const now = performance.now(); // Count nearby enemies (creeps + mobs) let nearbyEnemies = 0; let nearestEnemy = null; let nearestDistSq = Infinity; const rangeThresholdSq = 100; // 10 * 10 = 100 const playerPos = player.position; // Check hostile creeps - v8.01: forEach to for loop conversion if (creepWaveState.creeps) { for (let i = 0, len = creepWaveState.creeps.length; i < len; i++) { const creep = creepWaveState.creeps[i]; if (!creep || !creep.userData || creep.userData.team !== 'B') continue; const distSq = playerPos.distanceToSquared(creep.position); if (distSq < rangeThresholdSq) { nearbyEnemies++; if (distSq < nearestDistSq) { nearestDistSq = distSq; nearestEnemy = creep; } } } } // Check mobs - v8.01: forEach to for loop conversion for (let i = 0, len = worldState.mobs.length; i < len; i++) { const mob = worldState.mobs[i]; if (!mob.parent || mob.userData.hp <= 0) continue; const distSq = playerPos.distanceToSquared(mob.position); if (distSq < rangeThresholdSq) { nearbyEnemies++; if (distSq < nearestDistSq) { nearestDistSq = distSq; nearestEnemy = mob; } } } // === ABILITY PRIORITY LOGIC === // 1. HEAL if low HP if (playerHpPercent < 0.5 && isAbilityUnlocked('heal') && isAbilityReady('heal')) { useAbility('heal'); LANE_PUSH_AI.stats.abilitiesUsed++; return; } // 2. SHIELD WALL if taking damage and low HP if (playerHpPercent < 0.4 && nearbyEnemies > 0 && isAbilityUnlocked('shieldWall') && isAbilityReady('shieldWall')) { useAbility('shieldWall'); LANE_PUSH_AI.stats.abilitiesUsed++; return; } // 3. BERSERK (ultimate) for big wave or tower siege if (nearbyEnemies >= 4 && isAbilityUnlocked('berserk') && isAbilityReady('berserk')) { useAbility('berserk'); LANE_PUSH_AI.stats.abilitiesUsed++; return; } // 4. WHIRLWIND for wave clear (3+ enemies) if (nearbyEnemies >= 3 && isAbilityUnlocked('whirlwind') && isAbilityReady('whirlwind')) { useAbility('whirlwind'); LANE_PUSH_AI.stats.abilitiesUsed++; return; } // 5. WAR CRY for damage boost when fighting if (nearbyEnemies >= 2 && isAbilityUnlocked('warcry') && isAbilityReady('warcry')) { useAbility('warcry'); LANE_PUSH_AI.stats.abilitiesUsed++; return; } // 6. EXECUTE on low HP targets if (nearestEnemy && nearestEnemy.userData.hp / nearestEnemy.userData.maxHp < 0.3) { if (isAbilityUnlocked('execute') && isAbilityReady('execute')) { useAbility('execute'); LANE_PUSH_AI.stats.abilitiesUsed++; return; } } // 7. POWER STRIKE for single target burst if (nearestEnemy && nearestDist < 5 && isAbilityUnlocked('powerStrike') && isAbilityReady('powerStrike')) { useAbility('powerStrike'); LANE_PUSH_AI.stats.abilitiesUsed++; return; } // 8. DASH through enemy wave for damage + reposition // v8.22: Use pooled _tempDir instead of clone() if (nearbyEnemies >= 2 && isAbilityUnlocked('dash') && isAbilityReady('dash')) { // Face toward enemies first if (nearestEnemy) { const dir = LANE_PUSH_AI._tempDir.copy(nearestEnemy.position).sub(player.position).normalize(); player.rotation.y = Math.atan2(dir.x, dir.z); } useAbility('dash'); LANE_PUSH_AI.stats.abilitiesUsed++; return; } // 9. CHRONO-ECHO for sustained damage in big fights if (nearbyEnemies >= 3 && isAbilityUnlocked('chronoEcho') && isAbilityReady('chronoEcho')) { useAbility('chronoEcho'); LANE_PUSH_AI.stats.abilitiesUsed++; return; } } // ============================================ // END LANE PUSH AI SYSTEM // ============================================ // ============================================ // DOTA-STYLE AI BEHAVIOR SYSTEM // Integrates with TowerAggroSystem for tactical gameplay // ============================================ const DOTA_AI = { currentState: null, stateTimer: 0, targetLane: null, lastDecisionTime: 0, decisionCooldown: 2000, _moveDir: null, // v8.18: Pre-allocated for moveToward (lazy init) _safePos: null, // v8.18: Pre-allocated for retreatToSafeZone _basePos: null, // v8.18: Pre-allocated for pushToBase _pokeDir: null, // v8.18: Pre-allocated for pokeFromRange _pokePos: null, // v8.18: Pre-allocated for pokeFromRange // State-specific data waveCoordinator: { waitingForWave: false, nearestWave: null, followDistance: 12, attackRange: 8, _waveCenter: null // v8.18: Pre-allocated for wave center calculation (initialized on first use) }, towerDiver: { diveActive: false, diveStartTime: 0, maxDiveDuration: 6000, escapeThreshold: 0.3, towerHitsAbsorbed: 0 }, splitPusher: { currentLaneIndex: 0, rotationTimer: 0, rotationInterval: 15000, pressurePoints: [] }, lastHitter: { lastHitThreshold: 0.15, goldEfficiency: 0, perfectLastHits: 0, missedLastHits: 0 }, siegeMaster: { siegeMode: false, waveStackCount: 0, targetTower: null, patienceTimer: 0 }, gankHunter: { huntMode: false, lastKnownPlayerPos: null, ambushPosition: null, stalkDistance: 25 }, // Initialize based on current AI behavior init() { this.currentState = AI_BEHAVIOR.current; this.targetLane = this.selectOptimalLane(); }, // Main update - called every frame update(deltaTime, robot, player) { if (!robot || !AI_BEHAVIOR.active) return; const currentBehavior = AI_BEHAVIOR.current; const time = performance.now(); // State-specific AI logic switch(currentBehavior) { case 'waveCoordinator': this.runWaveCoordinator(robot, player, time); break; case 'towerDiver': this.runTowerDiver(robot, player, time); break; case 'splitPusher': this.runSplitPusher(robot, player, time); break; case 'lastHitter': this.runLastHitter(robot, player, time); break; case 'siegeMaster': this.runSiegeMaster(robot, player, time); break; case 'gankHunter': this.runGankHunter(robot, player, time); break; case 'lanePusher': this.runLanePusher(robot, player, time); break; } }, // WAVE COORDINATOR - Never attacks towers without creep cover runWaveCoordinator(robot, player, time) { const wc = this.waveCoordinator; // Find nearest friendly creep wave const friendlyCreeps = window.creeps ? window.creeps.filter(c => c.faction === 'enemy' && c.mesh // Enemy faction creeps are robot's allies ) : []; if (friendlyCreeps.length === 0) { // Wait at safe distance for next wave wc.waitingForWave = true; this.retreatToSafeZone(robot); return; } // Find wave center // v8.03: Converted forEach to for loop for performance // v8.18: Use pre-allocated vector for wave center calculation if (!wc._waveCenter) wc._waveCenter = new THREE.Vector3(); wc._waveCenter.set(0, 0, 0); for (let i = 0, len = friendlyCreeps.length; i < len; i++) { wc._waveCenter.add(friendlyCreeps[i].mesh.position); } wc._waveCenter.divideScalar(friendlyCreeps.length); wc.nearestWave = wc._waveCenter; // Calculate distance to wave // v10.34: Use distanceToSquared() in robot AI const distToWaveSq = robot.position.distanceToSquared(wc._waveCenter); const followDistSq = wc.followDistance * wc.followDistance; // Stay behind creeps, let them tank if (distToWaveSq > followDistSq) { // Move toward wave this.moveToward(robot, wc._waveCenter, 0.8); wc.waitingForWave = false; } else { // In position - attack if creeps are engaging const nearestTower = this.findNearestEnemyTower(robot); if (nearestTower && this.hasCreepCover(robot, nearestTower)) { // Safe to attack with creep cover // v10.34: Use distanceToSquared() for attack range check const attackRangeSq = wc.attackRange * wc.attackRange; if (robot.position.distanceToSquared(nearestTower.position) < attackRangeSq) { this.attackTarget(robot, nearestTower, 'tower'); } else { this.moveToward(robot, nearestTower.position, 0.6); } } else { // Stay with wave, attack enemy creeps const enemyCreep = this.findNearestEnemyCreep(robot); if (enemyCreep) { this.attackTarget(robot, enemyCreep, 'creep'); } } } }, // TOWER DIVER - Aggressive with smart aggro management runTowerDiver(robot, player, time) { const td = this.towerDiver; const nearestTower = this.findNearestEnemyTower(robot); if (!nearestTower) { // No towers, hunt player this.huntPlayer(robot, player); return; } // v10.34: Use distanceToSquared() for tower diver distance checks const distToTowerSq = robot.position.distanceToSquared(nearestTower.position); const robotHealthPercent = (robot.userData?.health || 100) / (robot.userData?.maxHealth || 100); // Check if we should dive if (!td.diveActive) { // Only dive if we have good HP and see opportunity // v10.34: Compare squared distances (20^2 = 400) if (robotHealthPercent > 0.7 && distToTowerSq < 400) { td.diveActive = true; td.diveStartTime = time; td.towerHitsAbsorbed = 0; } else { // Poke from range this.pokeFromRange(robot, nearestTower); return; } } // Active dive logic const diveDuration = time - td.diveStartTime; // Abort conditions if (robotHealthPercent < td.escapeThreshold || diveDuration > td.maxDiveDuration || td.towerHitsAbsorbed > 3) { td.diveActive = false; this.retreatToSafeZone(robot); return; } // Dive attack! // v10.34: Use squared distance (8^2 = 64) if (distToTowerSq < 64) { this.attackTarget(robot, nearestTower, 'tower'); // Track aggro if (typeof TowerAggroSystem !== 'undefined') { TowerAggroSystem.onPlayerAttack(nearestTower, 'tower'); } } else { this.moveToward(robot, nearestTower.position, 1.2); // Fast dive } }, // SPLIT PUSHER - Multi-lane pressure // v8.19: Pre-allocated target position to avoid allocation per call _splitPusherTarget: null, runSplitPusher(robot, player, time) { const sp = this.splitPusher; // Rotation timer sp.rotationTimer += 16; // ~60fps if (sp.rotationTimer > sp.rotationInterval) { sp.rotationTimer = 0; sp.currentLaneIndex = (sp.currentLaneIndex + 1) % 3; } // Get target lane position const lanes = [ { x: -50, z: 0 }, // Top lane { x: 0, z: 0 }, // Mid lane { x: 50, z: 0 } // Bot lane ]; const targetLane = lanes[sp.currentLaneIndex]; // v8.19: Use pre-allocated vector instead of new THREE.Vector3() per call if (!this._splitPusherTarget) this._splitPusherTarget = new THREE.Vector3(); const targetPos = this._splitPusherTarget.set(targetLane.x, 0, -80); // Push toward enemy base // v10.34: Use distanceToSquared() in split pusher const distToTargetSq = robot.position.distanceToSquared(targetPos); if (distToTargetSq > 225) { // 15^2 = 225 // Move to lane this.moveToward(robot, targetPos, 1.0); } else { // In lane - push! const nearestTower = this.findNearestEnemyTower(robot); const nearestCreep = this.findNearestEnemyCreep(robot); // Prioritize clearing creeps for fast push // v10.34: Use squared distance (12^2 = 144) if (nearestCreep && robot.position.distanceToSquared(nearestCreep.mesh.position) < 144) { this.attackTarget(robot, nearestCreep, 'creep'); } else if (nearestTower && this.hasCreepCover(robot, nearestTower)) { this.attackTarget(robot, nearestTower, 'tower'); } else { // Advance up the lane targetPos.z -= 10; this.moveToward(robot, targetPos, 0.7); } } }, // LAST HITTER - Gold/XP efficiency focus runLastHitter(robot, player, time) { const lh = this.lastHitter; // Find all enemy creeps const enemyCreeps = window.creeps ? window.creeps.filter(c => c.faction === 'player' && c.mesh // Player faction creeps are enemies to robot ) : []; if (enemyCreeps.length === 0) { // No creeps, wait in lane this.retreatToSafeZone(robot); return; } // Find creep closest to death (lowest HP percentage) // v8.03: Converted forEach to for loop for performance let lowestHPCreep = null; let lowestHPPercent = 1; for (let i = 0, len = enemyCreeps.length; i < len; i++) { const creep = enemyCreeps[i]; const hpPercent = (creep.health || 100) / (creep.maxHealth || 100); if (hpPercent < lowestHPPercent) { lowestHPPercent = hpPercent; lowestHPCreep = creep; } } // Only attack if creep is in last-hit range // v10.34: Use distanceToSquared() in last hitter if (lowestHPCreep && lowestHPPercent < lh.lastHitThreshold) { const distSq = robot.position.distanceToSquared(lowestHPCreep.mesh.position); if (distSq < 100) { // 10^2 = 100 this.attackTarget(robot, lowestHPCreep, 'creep'); lh.perfectLastHits++; } else { this.moveToward(robot, lowestHPCreep.mesh.position, 0.9); } } else { // Position for next last hit // v7.98: Use GlobalVec3Pool instead of clone() if (lowestHPCreep) { const safePos = GlobalVec3Pool.temp().copy(lowestHPCreep.mesh.position); safePos.z += 8; // Stay behind this.moveToward(robot, safePos, 0.5); } } }, // SIEGE MASTER - Patient tower destruction runSiegeMaster(robot, player, time) { const sm = this.siegeMaster; // Find target tower if (!sm.targetTower) { sm.targetTower = this.findNearestEnemyTower(robot); } if (!sm.targetTower) { // All towers down, push base this.pushToBase(robot); return; } // Count friendly creeps near tower // v7.80: distanceToSquared optimization const friendlyCreeps = window.creeps ? window.creeps.filter(c => { if (c.faction !== 'enemy' || !c.mesh) return false; return c.mesh.position.distanceToSquared(sm.targetTower.position) < 400; // 20*20=400 }) : []; sm.waveStackCount = friendlyCreeps.length; // Siege mode requires 4+ creeps if (sm.waveStackCount >= 4) { sm.siegeMode = true; sm.patienceTimer = 0; // Full siege - attack tower with creep army // v7.80: distanceToSquared optimization const distSq = robot.position.distanceToSquared(sm.targetTower.position); if (distSq < 100) { // 10*10=100 this.attackTarget(robot, sm.targetTower, 'tower'); } else { this.moveToward(robot, sm.targetTower.position, 0.8); } } else { sm.siegeMode = false; sm.patienceTimer += 16; // Wait for more creeps // v7.98: Use GlobalVec3Pool instead of clone() const waitPos = GlobalVec3Pool.temp().copy(sm.targetTower.position); waitPos.z += 25; // Safe distance this.moveToward(robot, waitPos, 0.4); } }, // GANK HUNTER - Roams hunting player // v7.80: distanceToSquared optimization // v7.98: GlobalVec3Pool for lastKnownPlayerPos tracking runGankHunter(robot, player, time) { const gh = this.gankHunter; if (!player) return; const distToPlayerSq = robot.position.distanceToSquared(player.position); // Update last known position (40*40=1600) // Note: lastKnownPlayerPos is stored so needs a real clone if (distToPlayerSq < 1600) { if (!gh.lastKnownPlayerPos) { gh.lastKnownPlayerPos = new THREE.Vector3(); } gh.lastKnownPlayerPos.copy(player.position); } const stalkDistSq = gh.stalkDistance * gh.stalkDistance; if (distToPlayerSq < stalkDistSq) { gh.huntMode = true; // Close in for the kill (8*8=64, 15*15=225) if (distToPlayerSq < 64) { // Attack! this.attackTarget(robot, { position: player.position }, 'player'); } else if (distToPlayerSq < 225) { // Sprint in this.moveToward(robot, player.position, 1.3); } else { // Stalk approach this.moveToward(robot, player.position, 0.9); } } else { gh.huntMode = false; // Search for player if (gh.lastKnownPlayerPos) { this.moveToward(robot, gh.lastKnownPlayerPos, 0.7); } else { // Patrol lanes looking for player this.patrolLanes(robot, time); } } }, // LANE PUSHER - Enhanced with TowerAggroSystem // v7.80: distanceToSquared optimization runLanePusher(robot, player, time) { // Use existing lane push system but integrate tower aggro awareness const nearestTower = this.findNearestEnemyTower(robot); if (nearestTower) { const hasCover = this.hasCreepCover(robot, nearestTower); if (hasCover) { // Safe to push with creeps const distSq = robot.position.distanceToSquared(nearestTower.position); if (distSq < 100) { // 10*10=100 this.attackTarget(robot, nearestTower, 'tower'); } else { this.moveToward(robot, nearestTower.position, 0.7); } } else { // Wait for creep wave const nearestCreep = this.findNearestFriendlyCreep(robot); if (nearestCreep) { this.moveToward(robot, nearestCreep.mesh.position, 0.6); } else { this.retreatToSafeZone(robot); } } } else { // No towers, push to base this.pushToBase(robot); } }, // Helper: Check if robot has creep cover against tower // v7.80: distanceToSquared optimization hasCreepCover(robot, tower) { if (typeof TowerAggroSystem !== 'undefined') { return TowerAggroSystem.hasPlayerCreepCover(tower); } // Fallback check (15*15=225) const friendlyCreeps = window.creeps ? window.creeps.filter(c => c.faction === 'enemy' && c.mesh && c.mesh.position.distanceToSquared(tower.position) < 225 ) : []; return friendlyCreeps.length >= 2; }, // Helper: Find nearest enemy tower // v10.34: Use distanceToSquared() to avoid sqrt // v8.03: Converted forEach to for loop for performance findNearestEnemyTower(robot) { if (!window.lanes) return null; let nearest = null; let nearestDistSq = Infinity; for (let i = 0, laneLen = window.lanes.length; i < laneLen; i++) { const lane = window.lanes[i]; if (lane.towers) { for (let j = 0, towerLen = lane.towers.length; j < towerLen; j++) { const tower = lane.towers[j]; if (tower.faction === 'player' && tower.mesh && tower.health > 0) { const distSq = robot.position.distanceToSquared(tower.mesh.position); if (distSq < nearestDistSq) { nearestDistSq = distSq; nearest = { position: tower.mesh.position, ...tower }; } } } } } return nearest; }, // Helper: Find nearest enemy creep // v10.34: Use distanceToSquared() to avoid sqrt // v8.03: Converted forEach to for loop for performance findNearestEnemyCreep(robot) { if (!window.creeps) return null; let nearest = null; let nearestDistSq = Infinity; for (let i = 0, len = window.creeps.length; i < len; i++) { const creep = window.creeps[i]; if (creep.faction === 'player' && creep.mesh) { const distSq = robot.position.distanceToSquared(creep.mesh.position); if (distSq < nearestDistSq) { nearestDistSq = distSq; nearest = creep; } } } return nearest; }, // Helper: Find nearest friendly creep // v10.34: Use distanceToSquared() to avoid sqrt // v8.03: Converted forEach to for loop for performance findNearestFriendlyCreep(robot) { if (!window.creeps) return null; let nearest = null; let nearestDistSq = Infinity; for (let i = 0, len = window.creeps.length; i < len; i++) { const creep = window.creeps[i]; if (creep.faction === 'enemy' && creep.mesh) { const distSq = robot.position.distanceToSquared(creep.mesh.position); if (distSq < nearestDistSq) { nearestDistSq = distSq; nearest = creep; } } } return nearest; }, // Helper: Move robot toward target // v8.18: Use pre-allocated vector to avoid allocation per call moveToward(robot, target, speedMultiplier = 1.0) { if (!this._moveDir) this._moveDir = new THREE.Vector3(); this._moveDir.subVectors(target, robot.position).normalize(); const speed = 0.15 * speedMultiplier; robot.position.add(this._moveDir.multiplyScalar(speed)); // Face movement direction robot.lookAt(target); }, // Helper: Attack a target attackTarget(robot, target, targetType) { // Trigger TowerAggroSystem if attacking tower if (targetType === 'tower' && typeof TowerAggroSystem !== 'undefined') { TowerAggroSystem.onPlayerAttack(target, 'tower'); } // Visual attack indicator if (robot.userData) { robot.userData.attacking = true; robot.userData.attackTarget = target; robot.userData.attackType = targetType; } }, // Helper: Retreat to safe zone // v8.18: Use pre-allocated vector retreatToSafeZone(robot) { if (!this._safePos) this._safePos = new THREE.Vector3(0, 0, 60); this.moveToward(robot, this._safePos, 0.6); }, // Helper: Push toward enemy base // v8.18: Use pre-allocated vector pushToBase(robot) { if (!this._basePos) this._basePos = new THREE.Vector3(0, 0, -100); this.moveToward(robot, this._basePos, 0.8); }, // Helper: Poke from range // v8.18: Use pre-allocated vectors pokeFromRange(robot, tower) { const pokeDistance = 18; if (!this._pokeDir) this._pokeDir = new THREE.Vector3(); if (!this._pokePos) this._pokePos = new THREE.Vector3(); this._pokeDir.subVectors(robot.position, tower.position).normalize(); this._pokePos.copy(tower.position).add(this._pokeDir.multiplyScalar(pokeDistance)); this.moveToward(robot, this._pokePos, 0.5); }, // Helper: Hunt player huntPlayer(robot, player) { if (player) { this.moveToward(robot, player.position, 1.1); } }, // Helper: Patrol lanes // v8.18: Cache patrol points to avoid allocation every call _patrolPoints: null, patrolLanes(robot, time) { if (!this._patrolPoints) { this._patrolPoints = [ new THREE.Vector3(-40, 0, 0), new THREE.Vector3(0, 0, -20), new THREE.Vector3(40, 0, 0) ]; } const index = Math.floor((time / 5000) % 3); this.moveToward(robot, this._patrolPoints[index], 0.6); }, // Helper: Select optimal lane selectOptimalLane() { // Return lane with most push potential return 'mid'; // Default to mid } }; // ============================================ // END DOTA-STYLE AI BEHAVIOR SYSTEM // ============================================ // ============================================ // v7.75: 5v5 DOTA HERO TEAM SYSTEM // Spawns hero agents as teammates with full Dota 2 hero roster (124 heroes) // ============================================ // Full 124 hero roster from data/games/heroes/index.json const DOTA_HERO_INDEX = { categories: { strength: { icon: "💪", color: 0xff4444 }, agility: { icon: "⚡", color: 0x44ff44 }, intelligence: { icon: "🧠", color: 0x4488ff }, universal: { icon: "🌟", color: 0xffaa00 } }, heroes: [ { id: "abaddon", name: "Abaddon", title: "Lord of Avernus", attr: "universal", icon: "🛡️", roles: ["support", "durable"] }, { id: "alchemist", name: "Alchemist", title: "Razzil Darkbrew", attr: "strength", icon: "🧪", roles: ["carry", "durable"] }, { id: "ancient_apparition", name: "Ancient Apparition", title: "Kaldr", attr: "intelligence", icon: "❄️", roles: ["support", "nuker"] }, { id: "anti_mage", name: "Anti-Mage", title: "Magina", attr: "agility", icon: "🔮", roles: ["carry", "escape"] }, { id: "arc_warden", name: "Arc Warden", title: "Zet", attr: "agility", icon: "⚡", roles: ["carry", "nuker"] }, { id: "axe", name: "Axe", title: "Mogul Khan", attr: "strength", icon: "🪓", roles: ["initiator", "durable"] }, { id: "bane", name: "Bane", title: "Atropos", attr: "universal", icon: "😈", roles: ["support", "disabler"] }, { id: "batrider", name: "Batrider", title: "Jin'zakk", attr: "universal", icon: "🦇", roles: ["initiator", "escape"] }, { id: "beastmaster", name: "Beastmaster", title: "Karroch", attr: "universal", icon: "🐗", roles: ["initiator", "durable"] }, { id: "bloodseeker", name: "Bloodseeker", title: "Strygwyr", attr: "agility", icon: "🩸", roles: ["carry", "nuker"] }, { id: "bounty_hunter", name: "Bounty Hunter", title: "Gondar", attr: "agility", icon: "💰", roles: ["escape", "nuker"] }, { id: "brewmaster", name: "Brewmaster", title: "Mangix", attr: "universal", icon: "🍺", roles: ["carry", "initiator"] }, { id: "bristleback", name: "Bristleback", title: "Rigwarl", attr: "strength", icon: "🦔", roles: ["carry", "durable"] }, { id: "broodmother", name: "Broodmother", title: "Black Arachnia", attr: "universal", icon: "🕷️", roles: ["carry", "pusher"] }, { id: "centaur_warrunner", name: "Centaur Warrunner", title: "Bradwarden", attr: "strength", icon: "🐴", roles: ["durable", "initiator"] }, { id: "chaos_knight", name: "Chaos Knight", title: "Nessaj", attr: "strength", icon: "⚔️", roles: ["carry", "durable"] }, { id: "chen", name: "Chen", title: "Holy Knight", attr: "universal", icon: "✝️", roles: ["support", "pusher"] }, { id: "clinkz", name: "Clinkz", title: "Bone Fletcher", attr: "agility", icon: "💀", roles: ["carry", "escape"] }, { id: "clockwerk", name: "Clockwerk", title: "Rattletrap", attr: "universal", icon: "⚙️", roles: ["initiator", "durable"] }, { id: "crystal_maiden", name: "Crystal Maiden", title: "Rylai", attr: "intelligence", icon: "💎", roles: ["support", "nuker"] }, { id: "dark_seer", name: "Dark Seer", title: "Ish'Kafel", attr: "universal", icon: "🌀", roles: ["initiator", "escape"] }, { id: "dark_willow", name: "Dark Willow", title: "Mireska", attr: "universal", icon: "🧚", roles: ["support", "nuker"] }, { id: "dawnbreaker", name: "Dawnbreaker", title: "Valora", attr: "strength", icon: "☀️", roles: ["carry", "durable"] }, { id: "dazzle", name: "Dazzle", title: "Shadow Priest", attr: "universal", icon: "💜", roles: ["support", "nuker"] }, { id: "death_prophet", name: "Death Prophet", title: "Krobelus", attr: "intelligence", icon: "👻", roles: ["carry", "pusher"] }, { id: "disruptor", name: "Disruptor", title: "Thrall", attr: "intelligence", icon: "🌩️", roles: ["support", "disabler"] }, { id: "doom", name: "Doom", title: "Lucifer", attr: "strength", icon: "🔥", roles: ["carry", "durable"] }, { id: "dragon_knight", name: "Dragon Knight", title: "Davion", attr: "strength", icon: "🐉", roles: ["carry", "durable"] }, { id: "drow_ranger", name: "Drow Ranger", title: "Traxex", attr: "agility", icon: "🏹", roles: ["carry", "disabler"] }, { id: "earth_spirit", name: "Earth Spirit", title: "Kaolin", attr: "strength", icon: "🪨", roles: ["initiator", "escape"] }, { id: "earthshaker", name: "Earthshaker", title: "Raigor", attr: "strength", icon: "🌍", roles: ["initiator", "disabler"] }, { id: "elder_titan", name: "Elder Titan", title: "Worldsmith", attr: "strength", icon: "🦣", roles: ["initiator", "durable"] }, { id: "ember_spirit", name: "Ember Spirit", title: "Xin", attr: "agility", icon: "🔥", roles: ["carry", "escape"] }, { id: "enchantress", name: "Enchantress", title: "Aiushtha", attr: "universal", icon: "🦌", roles: ["support", "pusher"] }, { id: "enigma", name: "Enigma", title: "Void Entity", attr: "universal", icon: "🌌", roles: ["disabler", "initiator"] }, { id: "faceless_void", name: "Faceless Void", title: "Darkterror", attr: "agility", icon: "⏱️", roles: ["carry", "initiator"] }, { id: "grimstroke", name: "Grimstroke", title: "Artist of Death", attr: "intelligence", icon: "🖌️", roles: ["support", "nuker"] }, { id: "gyrocopter", name: "Gyrocopter", title: "Aurel", attr: "agility", icon: "🚁", roles: ["carry", "nuker"] }, { id: "hoodwink", name: "Hoodwink", title: "Forest Rogue", attr: "agility", icon: "🐿️", roles: ["support", "nuker"] }, { id: "huskar", name: "Huskar", title: "Sacred Warrior", attr: "strength", icon: "🏹", roles: ["carry", "durable"] }, { id: "invoker", name: "Invoker", title: "Kael", attr: "universal", icon: "🌟", roles: ["carry", "nuker"] }, { id: "io", name: "Io", title: "Wisp", attr: "universal", icon: "⚪", roles: ["support", "escape"] }, { id: "jakiro", name: "Jakiro", title: "Twin Head Dragon", attr: "intelligence", icon: "🐲", roles: ["support", "pusher"] }, { id: "juggernaut", name: "Juggernaut", title: "Yurnero", attr: "agility", icon: "⚔️", roles: ["carry", "escape"] }, { id: "keeper_of_the_light", name: "Keeper of the Light", title: "Ezalor", attr: "universal", icon: "💡", roles: ["support", "nuker"] }, { id: "kunkka", name: "Kunkka", title: "Admiral", attr: "strength", icon: "⚓", roles: ["carry", "initiator"] }, { id: "legion_commander", name: "Legion Commander", title: "Tresdin", attr: "strength", icon: "🗡️", roles: ["carry", "durable"] }, { id: "leshrac", name: "Leshrac", title: "Tormented Soul", attr: "intelligence", icon: "⚡", roles: ["carry", "pusher"] }, { id: "lich", name: "Lich", title: "Ethreain", attr: "intelligence", icon: "🥶", roles: ["support", "nuker"] }, { id: "lifestealer", name: "Lifestealer", title: "N'aix", attr: "strength", icon: "🧟", roles: ["carry", "durable"] }, { id: "lina", name: "Lina", title: "Slayer", attr: "intelligence", icon: "🔥", roles: ["carry", "nuker"] }, { id: "lion", name: "Lion", title: "Demon Witch", attr: "intelligence", icon: "🦁", roles: ["support", "disabler"] }, { id: "lone_druid", name: "Lone Druid", title: "Sylla", attr: "universal", icon: "🐻", roles: ["carry", "pusher"] }, { id: "luna", name: "Luna", title: "Moon Rider", attr: "agility", icon: "🌙", roles: ["carry", "nuker"] }, { id: "lycan", name: "Lycan", title: "Banehallow", attr: "universal", icon: "🐺", roles: ["carry", "pusher"] }, { id: "magnus", name: "Magnus", title: "Magnataur", attr: "strength", icon: "🦏", roles: ["initiator", "escape"] }, { id: "marci", name: "Marci", title: "Faithful Sidekick", attr: "universal", icon: "👊", roles: ["carry", "support"] }, { id: "mars", name: "Mars", title: "God of War", attr: "strength", icon: "🛡️", roles: ["carry", "initiator"] }, { id: "medusa", name: "Medusa", title: "Gorgon", attr: "agility", icon: "🐍", roles: ["carry", "durable"] }, { id: "meepo", name: "Meepo", title: "Geomancer", attr: "agility", icon: "🐭", roles: ["carry", "escape"] }, { id: "mirana", name: "Mirana", title: "Princess of the Moon", attr: "universal", icon: "🌙", roles: ["carry", "support"] }, { id: "monkey_king", name: "Monkey King", title: "Sun Wukong", attr: "agility", icon: "🐵", roles: ["carry", "escape"] }, { id: "morphling", name: "Morphling", title: "Water Elemental", attr: "agility", icon: "💧", roles: ["carry", "escape"] }, { id: "muerta", name: "Muerta", title: "Master of Death", attr: "intelligence", icon: "💀", roles: ["carry", "nuker"] }, { id: "naga_siren", name: "Naga Siren", title: "Slithice", attr: "agility", icon: "🧜", roles: ["carry", "support"] }, { id: "natures_prophet", name: "Nature's Prophet", title: "Tequoia", attr: "intelligence", icon: "🌲", roles: ["carry", "pusher"] }, { id: "necrophos", name: "Necrophos", title: "Rotund'jere", attr: "intelligence", icon: "☠️", roles: ["carry", "nuker"] }, { id: "night_stalker", name: "Night Stalker", title: "Balanar", attr: "strength", icon: "🌃", roles: ["carry", "initiator"] }, { id: "nyx_assassin", name: "Nyx Assassin", title: "Anub'arak", attr: "agility", icon: "🪲", roles: ["disabler", "initiator"] }, { id: "ogre_magi", name: "Ogre Magi", title: "Aggron", attr: "strength", icon: "🧌", roles: ["support", "durable"] }, { id: "omniknight", name: "Omniknight", title: "Purist", attr: "strength", icon: "⚔️", roles: ["support", "durable"] }, { id: "oracle", name: "Oracle", title: "Nerif", attr: "intelligence", icon: "🔮", roles: ["support", "nuker"] }, { id: "outworld_destroyer", name: "Outworld Destroyer", title: "Harbinger", attr: "intelligence", icon: "🌑", roles: ["carry", "nuker"] }, { id: "pangolier", name: "Pangolier", title: "Donté Panlin", attr: "universal", icon: "🛡️", roles: ["carry", "initiator"] }, { id: "phantom_assassin", name: "Phantom Assassin", title: "Mortred", attr: "agility", icon: "🗡️", roles: ["carry", "escape"] }, { id: "phantom_lancer", name: "Phantom Lancer", title: "Azwraith", attr: "agility", icon: "👤", roles: ["carry", "escape"] }, { id: "phoenix", name: "Phoenix", title: "Icarus", attr: "strength", icon: "🐦‍🔥", roles: ["support", "nuker"] }, { id: "primal_beast", name: "Primal Beast", title: "Ancient Predator", attr: "strength", icon: "🦍", roles: ["carry", "initiator"] }, { id: "puck", name: "Puck", title: "Faerie Dragon", attr: "intelligence", icon: "🦋", roles: ["initiator", "escape"] }, { id: "pudge", name: "Pudge", title: "Butcher", attr: "strength", icon: "🪝", roles: ["disabler", "initiator"] }, { id: "pugna", name: "Pugna", title: "Oblivion", attr: "intelligence", icon: "👹", roles: ["nuker", "pusher"] }, { id: "queen_of_pain", name: "Queen of Pain", title: "Akasha", attr: "intelligence", icon: "👑", roles: ["carry", "nuker"] }, { id: "razor", name: "Razor", title: "Lightning Revenant", attr: "agility", icon: "⚡", roles: ["carry", "durable"] }, { id: "riki", name: "Riki", title: "Stealth Assassin", attr: "agility", icon: "🥷", roles: ["carry", "escape"] }, { id: "rubick", name: "Rubick", title: "Grand Magus", attr: "intelligence", icon: "✨", roles: ["support", "nuker"] }, { id: "sand_king", name: "Sand King", title: "Crixalis", attr: "strength", icon: "🦂", roles: ["initiator", "escape"] }, { id: "shadow_demon", name: "Shadow Demon", title: "Eredar", attr: "intelligence", icon: "👿", roles: ["support", "disabler"] }, { id: "shadow_fiend", name: "Shadow Fiend", title: "Nevermore", attr: "agility", icon: "😈", roles: ["carry", "nuker"] }, { id: "shadow_shaman", name: "Shadow Shaman", title: "Rhasta", attr: "intelligence", icon: "🐍", roles: ["support", "pusher"] }, { id: "silencer", name: "Silencer", title: "Nortrom", attr: "intelligence", icon: "🤫", roles: ["carry", "disabler"] }, { id: "skywrath_mage", name: "Skywrath Mage", title: "Dragonus", attr: "intelligence", icon: "🦅", roles: ["support", "nuker"] }, { id: "slardar", name: "Slardar", title: "Slithereen Guard", attr: "strength", icon: "🐟", roles: ["carry", "initiator"] }, { id: "slark", name: "Slark", title: "Nightcrawler", attr: "agility", icon: "🦈", roles: ["carry", "escape"] }, { id: "snapfire", name: "Snapfire", title: "Beatrix", attr: "universal", icon: "🔫", roles: ["support", "nuker"] }, { id: "sniper", name: "Sniper", title: "Kardel", attr: "agility", icon: "🎯", roles: ["carry", "nuker"] }, { id: "spectre", name: "Spectre", title: "Mercurial", attr: "agility", icon: "👤", roles: ["carry", "durable"] }, { id: "spirit_breaker", name: "Spirit Breaker", title: "Barathrum", attr: "strength", icon: "🐂", roles: ["carry", "initiator"] }, { id: "storm_spirit", name: "Storm Spirit", title: "Raijin", attr: "intelligence", icon: "⛈️", roles: ["carry", "escape"] }, { id: "sven", name: "Sven", title: "Rogue Knight", attr: "strength", icon: "🗡️", roles: ["carry", "durable"] }, { id: "techies", name: "Techies", title: "Squee & Spleen", attr: "universal", icon: "💣", roles: ["nuker", "disabler"] }, { id: "templar_assassin", name: "Templar Assassin", title: "Lanaya", attr: "agility", icon: "🏹", roles: ["carry", "escape"] }, { id: "terrorblade", name: "Terrorblade", title: "Soul Keeper", attr: "agility", icon: "😈", roles: ["carry", "pusher"] }, { id: "tidehunter", name: "Tidehunter", title: "Leviathan", attr: "strength", icon: "🐙", roles: ["initiator", "durable"] }, { id: "timbersaw", name: "Timbersaw", title: "Rizzrack", attr: "strength", icon: "🪚", roles: ["carry", "nuker"] }, { id: "tinker", name: "Tinker", title: "Boush", attr: "intelligence", icon: "🔧", roles: ["carry", "nuker"] }, { id: "tiny", name: "Tiny", title: "Stone Giant", attr: "strength", icon: "🪨", roles: ["carry", "nuker"] }, { id: "treant_protector", name: "Treant Protector", title: "Rooftrellen", attr: "strength", icon: "🌳", roles: ["support", "durable"] }, { id: "troll_warlord", name: "Troll Warlord", title: "Jah'rakal", attr: "agility", icon: "🧌", roles: ["carry", "pusher"] }, { id: "tusk", name: "Tusk", title: "Ymir", attr: "strength", icon: "🐘", roles: ["initiator", "nuker"] }, { id: "underlord", name: "Underlord", title: "Vrogros", attr: "strength", icon: "👹", roles: ["durable", "escape"] }, { id: "undying", name: "Undying", title: "Almighty Dirge", attr: "strength", icon: "🧟", roles: ["support", "durable"] }, { id: "ursa", name: "Ursa", title: "Ulfsaar", attr: "agility", icon: "🐻", roles: ["carry", "durable"] }, { id: "vengeful_spirit", name: "Vengeful Spirit", title: "Shendelzare", attr: "agility", icon: "👼", roles: ["support", "initiator"] }, { id: "venomancer", name: "Venomancer", title: "Lesale", attr: "universal", icon: "🐍", roles: ["support", "pusher"] }, { id: "viper", name: "Viper", title: "Netherdrake", attr: "agility", icon: "🐍", roles: ["carry", "durable"] }, { id: "visage", name: "Visage", title: "Necro'lic", attr: "universal", icon: "👻", roles: ["support", "pusher"] }, { id: "void_spirit", name: "Void Spirit", title: "Inai", attr: "universal", icon: "💨", roles: ["carry", "escape"] }, { id: "warlock", name: "Warlock", title: "Demnok Lannik", attr: "intelligence", icon: "📕", roles: ["support", "initiator"] }, { id: "weaver", name: "Weaver", title: "Skitskurr", attr: "agility", icon: "🪲", roles: ["carry", "escape"] }, { id: "windranger", name: "Windranger", title: "Lyralei", attr: "universal", icon: "🍃", roles: ["carry", "support"] }, { id: "winter_wyvern", name: "Winter Wyvern", title: "Auroth", attr: "universal", icon: "🐉", roles: ["support", "disabler"] }, { id: "witch_doctor", name: "Witch Doctor", title: "Zharvakko", attr: "intelligence", icon: "🎭", roles: ["support", "nuker"] }, { id: "wraith_king", name: "Wraith King", title: "Ostarion", attr: "strength", icon: "👑", roles: ["carry", "durable"] }, { id: "zeus", name: "Zeus", title: "Lord of Olympus", attr: "intelligence", icon: "⚡", roles: ["nuker"] } ] }; // Generate stats based on attribute type function generateHeroStats(hero) { const baseStats = { strength: { hp: 650, damage: 55, armor: 4, speed: 0.85, hpRegen: 3 }, agility: { hp: 480, damage: 65, armor: 3, speed: 1.05, hpRegen: 2 }, intelligence: { hp: 420, damage: 45, armor: 2, speed: 0.9, hpRegen: 2 }, universal: { hp: 550, damage: 52, armor: 3, speed: 0.95, hpRegen: 2.5 } }; const base = baseStats[hero.attr] || baseStats.universal; // Add some randomness for variety const variance = 0.15; return { hp: Math.round(base.hp * (1 + (Math.random() - 0.5) * variance)), damage: Math.round(base.damage * (1 + (Math.random() - 0.5) * variance)), armor: Math.round(base.armor * (1 + (Math.random() - 0.5) * variance)), speed: base.speed * (1 + (Math.random() - 0.5) * variance * 0.5), hpRegen: base.hpRegen }; } const DotaHeroTeamSystem = { allies: [], enemies: [], matchActive: false, matchStartTime: 0, // v8.19: Pre-allocated vectors for hot path functions _moveDir: null, _laneTarget: null, // v8.22: Pooled spawn position vector to avoid clone() allocations _spawnPos: null, _getSpawnPos() { if (!this._spawnPos) this._spawnPos = new THREE.Vector3(); return this._spawnPos; }, config: { allySpawnOffset: new THREE.Vector3(0, 0, 80), // Behind player base enemySpawnOffset: new THREE.Vector3(0, 0, -80), // Enemy base laneAssignments: ['top', 'mid', 'mid', 'bot', 'roam'], respawnTime: 15000 // 15 seconds }, // Get random heroes for a team from 124-hero database pickRandomHeroes(count, excludeIds = []) { const available = DOTA_HERO_INDEX.heroes.filter(h => !excludeIds.includes(h.id)); const picked = []; for (let i = 0; i < count && available.length > 0; i++) { const idx = Math.floor(Math.random() * available.length); picked.push(available.splice(idx, 1)[0]); } return picked; }, // Start a 5v5 match with random heroes from 124-hero database startMatch() { if (this.matchActive) return; console.log('[DOTA 5v5] Starting match with full hero roster...'); this.matchActive = true; this.matchStartTime = performance.now(); // Pick heroes - player gets random hero display, 4 AI allies const allyHeroes = this.pickRandomHeroes(4); const excludeIds = allyHeroes.map(h => h.id); const enemyHeroes = this.pickRandomHeroes(5, excludeIds); // Log hero picks console.log('[DOTA 5v5] Allied team:', allyHeroes.map(h => h.name).join(', ')); console.log('[DOTA 5v5] Enemy team:', enemyHeroes.map(h => h.name).join(', ')); // Spawn allied heroes allyHeroes.forEach((heroData, idx) => { const hero = this.spawnHero(heroData, 'ally', this.config.laneAssignments[idx + 1]); if (hero) this.allies.push(hero); }); // Spawn enemy heroes enemyHeroes.forEach((heroData, idx) => { const hero = this.spawnHero(heroData, 'enemy', this.config.laneAssignments[idx]); if (hero) this.enemies.push(hero); }); // Show match start notification this.showMatchNotification(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[DOTA 5v5] Match started: ${this.allies.length} allies vs ${this.enemies.length} enemies`); }, // Spawn a hero agent using 124-hero database spawnHero(heroData, faction, lane) { if (!heroData || !scene) return null; // Generate stats based on hero attribute const stats = generateHeroStats(heroData); // Get color from attribute category const attrColor = DOTA_HERO_INDEX.categories[heroData.attr]?.color || 0xffffff; const isAlly = faction === 'ally'; // v8.22: Use pooled vector with copy() instead of clone() const spawnPos = this._getSpawnPos().copy( isAlly ? this.config.allySpawnOffset : this.config.enemySpawnOffset ); // Offset by lane if (lane === 'top') spawnPos.x -= 40; else if (lane === 'bot') spawnPos.x += 40; spawnPos.x += (Math.random() - 0.5) * 10; spawnPos.z += (Math.random() - 0.5) * 10; // Create hero mesh const heroGroup = new THREE.Group(); // Body - humanoid shape with attribute color const bodyGeom = new THREE.CapsuleGeometry(0.4, 1.2, 8, 16); const bodyMat = new THREE.MeshStandardMaterial({ color: attrColor, metalness: 0.3, roughness: 0.7 }); const body = new THREE.Mesh(bodyGeom, bodyMat); body.position.y = 1.0; body.castShadow = true; heroGroup.add(body); // Faction indicator ring const ringGeom = new THREE.RingGeometry(0.6, 0.8, 32); const ringMat = new THREE.MeshBasicMaterial({ color: isAlly ? 0x44ff44 : 0xff4444, side: THREE.DoubleSide, transparent: true, opacity: 0.7 }); const ring = new THREE.Mesh(ringGeom, ringMat); ring.rotation.x = -Math.PI / 2; ring.position.y = 0.05; heroGroup.add(ring); // Health bar above head const hpBarBg = new THREE.Mesh( new THREE.PlaneGeometry(1.2, 0.15), new THREE.MeshBasicMaterial({ color: 0x333333 }) ); hpBarBg.position.y = 2.5; hpBarBg.rotation.x = 0; heroGroup.add(hpBarBg); const hpBarFill = new THREE.Mesh( new THREE.PlaneGeometry(1.18, 0.12), new THREE.MeshBasicMaterial({ color: isAlly ? 0x44ff44 : 0xff4444 }) ); hpBarFill.position.y = 2.5; hpBarFill.position.z = 0.01; heroGroup.add(hpBarFill); // Name label with icon (sprite) const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 64; const ctx = canvas.getContext('2d'); ctx.fillStyle = isAlly ? '#44ff44' : '#ff4444'; ctx.font = 'bold 28px Arial'; ctx.textAlign = 'center'; ctx.fillText(`${heroData.icon || ''} ${heroData.name}`, 128, 40); const labelTexture = new THREE.CanvasTexture(canvas); const labelMat = new THREE.SpriteMaterial({ map: labelTexture, transparent: true }); const label = new THREE.Sprite(labelMat); label.position.y = 2.9; label.scale.set(2.5, 0.6, 1); heroGroup.add(label); // Add title label below name const titleCanvas = document.createElement('canvas'); titleCanvas.width = 256; titleCanvas.height = 32; const titleCtx = titleCanvas.getContext('2d'); titleCtx.fillStyle = '#aaaaaa'; titleCtx.font = 'italic 18px Arial'; titleCtx.textAlign = 'center'; titleCtx.fillText(heroData.title || '', 128, 20); const titleTexture = new THREE.CanvasTexture(titleCanvas); const titleMat = new THREE.SpriteMaterial({ map: titleTexture, transparent: true }); const titleLabel = new THREE.Sprite(titleMat); titleLabel.position.y = 2.55; titleLabel.scale.set(2, 0.25, 1); heroGroup.add(titleLabel); heroGroup.position.copy(spawnPos); scene.add(heroGroup); // Generate ability names based on roles const abilityNames = this.generateAbilityNames(heroData); // Create hero data object with generated stats const hero = { id: `hero_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`, heroId: heroData.id, data: heroData, faction: faction, lane: lane, mesh: heroGroup, hpBar: hpBarFill, position: heroGroup.position, hp: stats.hp, maxHp: stats.hp, damage: stats.damage, armor: stats.armor, speed: stats.speed, hpRegen: stats.hpRegen, level: 1, xp: 0, gold: 0, kills: 0, deaths: 0, assists: 0, alive: true, respawnTime: 0, currentTarget: null, lastAttackTime: 0, attackCooldown: 1000 / stats.speed, abilities: abilityNames.map((name, i) => ({ name: name, level: i === 0 ? 1 : 0, cooldown: 0, maxCooldown: [8000, 12000, 15000, 60000][i] })), aiState: 'laning', targetPosition: null, roles: heroData.roles || [] }; return hero; }, // Generate ability names based on hero roles generateAbilityNames(heroData) { const roleAbilities = { carry: ['Power Strike', 'Fury Slash', 'Battle Rage', 'Rampage'], support: ['Heal Wave', 'Shield Aura', 'Mana Gift', 'Mass Salvation'], nuker: ['Arcane Blast', 'Chain Lightning', 'Meteor Strike', 'Devastation'], disabler: ['Stun Bolt', 'Root Trap', 'Silence Field', 'Total Lockdown'], initiator: ['Charge', 'Leap Attack', 'Battle Cry', 'Grand Entrance'], durable: ['Iron Skin', 'Regeneration', 'Taunt', 'Unbreakable'], escape: ['Blink', 'Phase Shift', 'Invisibility', 'Dimensional Rift'], pusher: ['Summon Minions', 'Tower Damage', 'Mass Assault', 'Siege Mode'] }; const primaryRole = (heroData.roles && heroData.roles[0]) || 'carry'; return roleAbilities[primaryRole] || roleAbilities.carry; }, // Update all heroes // v8.03: Converted forEach to for loop for performance update(deltaTime) { if (!this.matchActive) return; const time = performance.now(); // Update allies for (let i = 0, len = this.allies.length; i < len; i++) { this.updateHero(this.allies[i], time, deltaTime); } // Update enemies for (let i = 0, len = this.enemies.length; i < len; i++) { this.updateHero(this.enemies[i], time, deltaTime); } // Check for respawns this.checkRespawns(time); }, // Update single hero AI updateHero(hero, time, deltaTime) { if (!hero.alive) return; if (!hero.mesh) return; // Make HP bar face camera if (hero.hpBar && camera) { hero.hpBar.parent.lookAt(camera.position); } // Update HP bar scale if (hero.hpBar) { const hpPercent = hero.hp / hero.maxHp; hero.hpBar.scale.x = Math.max(0.01, hpPercent); hero.hpBar.position.x = (hpPercent - 1) * 0.59; } // AI behavior based on role and state const isAlly = hero.faction === 'ally'; const enemies = isAlly ? this.enemies : this.allies; const allies = isAlly ? this.allies : this.enemies; // Find nearest enemy // v8.03: Converted forEach to for loop for performance let nearestEnemy = null; let nearestDistSq = Infinity; for (let i = 0, len = enemies.length; i < len; i++) { const e = enemies[i]; if (!e.alive || !e.mesh) continue; const dSq = hero.position.distanceToSquared(e.position); if (dSq < nearestDistSq) { nearestDistSq = dSq; nearestEnemy = e; } } const attackRangeSq = 64; // 8 units const aggroRangeSq = 400; // 20 units // Combat logic if (nearestEnemy && nearestDistSq < aggroRangeSq) { hero.aiState = 'fighting'; hero.currentTarget = nearestEnemy; if (nearestDistSq < attackRangeSq) { // In attack range - attack! if (time - hero.lastAttackTime > hero.attackCooldown) { this.heroAttack(hero, nearestEnemy, time); } } else { // Move toward enemy this.moveHeroToward(hero, nearestEnemy.position, deltaTime); } } else { // Lane pushing behavior hero.aiState = 'laning'; hero.currentTarget = null; // Get lane target position const laneTarget = this.getLaneTargetPosition(hero); if (laneTarget) { this.moveHeroToward(hero, laneTarget, deltaTime); } } }, // Hero attacks target heroAttack(attacker, target, time) { attacker.lastAttackTime = time; // Calculate damage (reduced by armor) const damageReduction = target.armor * 0.06; const finalDamage = attacker.damage * (1 - Math.min(damageReduction, 0.8)); target.hp -= finalDamage; // Visual feedback if (target.mesh) { // Flash red target.mesh.children[0].material.emissive.setHex(0xff0000); setTimeout(() => { if (target.mesh?.children[0]?.material) { target.mesh.children[0].material.emissive.setHex(0x000000); } }, 100); } // Damage floater if (typeof spawnFloater === 'function') { spawnFloater(target.position, `-${Math.round(finalDamage)}`, '#ff4444'); } // Check for kill if (target.hp <= 0) { this.heroKilled(target, attacker); } }, // Hero was killed heroKilled(victim, killer) { victim.alive = false; victim.deaths++; victim.respawnTime = performance.now() + this.config.respawnTime; if (killer) { killer.kills++; killer.gold += 200 + victim.level * 50; killer.xp += 100 + victim.level * 25; } // Hide mesh if (victim.mesh) { victim.mesh.visible = false; } // Death notification const killerName = killer ? killer.data.name : 'Creeps'; // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[DOTA] ${killerName} killed ${victim.data.name}!`); // Show kill notification if (typeof spawnFloater === 'function' && victim.position) { spawnFloater(victim.position, `💀 ${victim.data.name}`, '#ff0000'); } }, // Check for hero respawns // v8.03: Converted forEach to for loop for performance checkRespawns(time) { const allHeroes = [...this.allies, ...this.enemies]; for (let i = 0, len = allHeroes.length; i < len; i++) { const hero = allHeroes[i]; if (!hero.alive && time >= hero.respawnTime) { this.respawnHero(hero); } } }, // Respawn a hero respawnHero(hero) { hero.alive = true; hero.hp = hero.maxHp; // Move to spawn // v8.22: Use pooled vector with copy() instead of clone() const isAlly = hero.faction === 'ally'; const spawnPos = this._getSpawnPos().copy( isAlly ? this.config.allySpawnOffset : this.config.enemySpawnOffset ); spawnPos.x += (Math.random() - 0.5) * 20; hero.position.copy(spawnPos); // Show mesh if (hero.mesh) { hero.mesh.visible = true; } // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[DOTA] ${hero.data.name} respawned!`); }, // Move hero toward target position // v8.19: Use pre-allocated vector to avoid allocation per call moveHeroToward(hero, target, deltaTime) { if (!this._moveDir) this._moveDir = new THREE.Vector3(); const direction = this._moveDir.subVectors(target, hero.position).normalize(); const moveSpeed = hero.speed * 8 * deltaTime; hero.position.add(direction.multiplyScalar(moveSpeed)); // Face movement direction if (hero.mesh) { hero.mesh.lookAt(target.x, hero.position.y, target.z); } }, // Get lane target position for hero // v8.19: Use pre-allocated vector to avoid allocation per call getLaneTargetPosition(hero) { const isAlly = hero.faction === 'ally'; const pushDirection = isAlly ? -1 : 1; // Allies push toward negative Z // Lane X positions const laneX = { top: -40, mid: 0, bot: 40, roam: (Math.random() - 0.5) * 60 }; const x = laneX[hero.lane] || 0; // Push forward in lane const targetZ = hero.position.z + pushDirection * 30; if (!this._laneTarget) this._laneTarget = new THREE.Vector3(); return this._laneTarget.set(x, 0, Math.max(-90, Math.min(90, targetZ))); }, // Show match start notification showMatchNotification() { const overlay = document.createElement('div'); overlay.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.9); padding: 40px 60px; border-radius: 15px; color: white; font-family: sans-serif; text-align: center; z-index: 10001; animation: fadeIn 0.5s ease-out; border: 3px solid #ffd700; `; const allyNames = this.allies.map(h => `${h.data.icon} ${h.data.name}`).join('
'); const enemyNames = this.enemies.map(h => `${h.data.icon} ${h.data.name}`).join('
'); overlay.innerHTML = `

⚔️ 5v5 MATCH ⚔️

RADIANT

You (Player)
${allyNames}
VS

DIRE

${enemyNames}

Match starting in 3 seconds...

`; document.body.appendChild(overlay); setTimeout(() => { overlay.style.opacity = '0'; overlay.style.transition = 'opacity 0.5s'; setTimeout(() => overlay.remove(), 500); }, 3000); }, // End match // v8.03: Converted forEach to for loop for performance endMatch() { this.matchActive = false; // Remove all hero meshes const allHeroes = [...this.allies, ...this.enemies]; for (let i = 0, len = allHeroes.length; i < len; i++) { const hero = allHeroes[i]; if (hero.mesh) { scene.remove(hero.mesh); } } this.allies = []; this.enemies = []; console.log('[DOTA 5v5] Match ended'); }, // Get match stats getMatchStats() { const allyKills = this.allies.reduce((sum, h) => sum + h.kills, 0); const allyDeaths = this.allies.reduce((sum, h) => sum + h.deaths, 0); const enemyKills = this.enemies.reduce((sum, h) => sum + h.kills, 0); const enemyDeaths = this.enemies.reduce((sum, h) => sum + h.deaths, 0); return { radiant: { kills: allyKills + enemyDeaths, deaths: allyDeaths }, dire: { kills: enemyKills + allyDeaths, deaths: enemyDeaths }, duration: performance.now() - this.matchStartTime }; } }; // Expose to global for UI window.DotaHeroTeamSystem = DotaHeroTeamSystem; window.startDotaMatch = () => DotaHeroTeamSystem.startMatch(); // ============================================ // END 5v5 DOTA HERO TEAM SYSTEM // ============================================ // WASD Keyboard controls const keys = { w: false, a: false, s: false, d: false }; // v12.20: Mouse state for vehicle combat (MAKO system) const mouseState = { left: false, right: false }; document.addEventListener('mousedown', (e) => { if (e.button === 0) mouseState.left = true; if (e.button === 2) mouseState.right = true; }); document.addEventListener('mouseup', (e) => { if (e.button === 0) mouseState.left = false; if (e.button === 2) mouseState.right = false; }); // Prevent context menu on right-click when in vehicle document.addEventListener('contextmenu', (e) => { if (typeof MakoVehicleSystem !== 'undefined' && MakoVehicleSystem.playerInVehicle) { e.preventDefault(); } }); // Persistent Game Data (saved to localStorage) let gameData = { version: VERSION, playtime: 0, totalCycles: 0, // v6.92: Persistent cycle counter lastPlayed: null, hasSeenTutorial: false, // v4.0: Tutorial tracking inventory: [], droppedItems: {}, // v6.34: Dropped items by planet ID { planetId: [{x, y, z, items: [...]}] } skills: { mining: { level: 1, xp: 0 }, wood: { level: 1, xp: 0 }, combat: { level: 1, xp: 0 }, fishing: { level: 1, xp: 0 }, cooking: { level: 1, xp: 0 }, crafting: { level: 1, xp: 0 }, alchemy: { level: 1, xp: 0 } // v6.1: New Alchemy skill }, player: { hp: CONFIG.PLAYER_MAX_HP, maxHp: CONFIG.PLAYER_MAX_HP, // v6.18: Persistent world state lastPlanetId: null, // Which planet they were on lastPosition: null, // {x, y, z} position on that planet lastRotation: null // Y rotation (facing direction) }, visitedPlanets: [], // v7.4: Track visit counts per planet planetVisitCounts: {}, // { planetId: visitCount } // v6.92: Persistent planet destruction/escape tracking destroyedPlanets: [], // IDs of planets destroyed by collision escapedPlanets: [], // IDs of planets that escaped orbit // v6.95: Player identity for universe ignition tracking playerName: 'Pioneer ' + Math.floor(Math.random() * 9000 + 1000), // v6.86: Galaxy Discovery System - track which galaxy generation we're on galaxyNumber: 1, // Current galaxy number (starts at 1) galaxySeed: 'OMNIVERSE', // Current galaxy seed for unique generation galaxiesDiscovered: 1, // Total galaxies discovered (for stats) galaxyHistory: [], // Array of previously discovered galaxies with their states // v6.95: First universe ignition - recorded when OMNIVERSE is first generated firstIgnition: null, // { ignitedBy, ignitedAt, ignitionSignature } statistics: { treesChopped: 0, oresMined: 0, mobsKilled: 0, fishCaught: 0, itemsCrafted: 0, fishCooked: 0, // v4.2: New stats poisDiscovered: 0, totalDamageDealt: 0, bossesDefeated: 0, distanceTraveled: 0 }, // v4.2: Player rank tracking playerRank: { points: 0, lastTitle: 'Novice Explorer' }, // v4.2: Discovered POIs by planet discoveredPOIs: {}, // v4.1: Achievement System achievements: {}, // v4.1: Daily Challenge System dailyChallenge: { lastGenerated: null, completed: false, current: null, streak: 0, bestStreak: 0 }, // v4.4: Prestige System prestige: { level: 0, totalLifetimePoints: 0, bonuses: { xpMultiplier: 1.0, startingSkillBonus: 0 } }, // v12.17: BATTERY CORE - Permanent progression (NEVER resets, even on prestige) // This is the robot's core energy matrix - upgrades are eternal batteryCore: { level: 1, // Battery Core Level (starts at 1) xp: 0, // Current XP toward next level totalXP: 0, // Lifetime XP earned (for stats) capacityBonus: 0, // Bonus max battery from levels efficiencyBonus: 0, // Bonus power efficiency from levels regenBonus: 0, // Bonus power regen from levels // Milestone unlocks (permanent perks) milestones: { level5: false, // First milestone level10: false, // Second milestone level25: false, // Third milestone level50: false, // Fourth milestone level100: false // Mastery milestone } }, // v12.19: ADAPTIVE LEARNING SYSTEM - "What you can't do today, you might be able to tomorrow" // The AI learns from player behavior patterns and tailors the experience over time // This data is PERMANENT - the game literally gets smarter about serving you adaptiveAI: { version: 1, // Behavioral observations - what the AI has learned about this player observations: { // Playstyle profile (0-1 scale, learned over time) playstyle: { explorer: 0.5, // Prefers exploration vs combat combatant: 0.5, // Prefers combat vs exploration gatherer: 0.5, // Focuses on resource collection builder: 0.5, // Engages with building/crafting speedrunner: 0.5, // Rushes through content completionist: 0.5 // Tries to do everything }, // Skill preferences (what they gravitate toward) preferredSkills: {}, // { skillName: usageCount } preferredAbilities: {}, // { abilityName: usageCount } preferredBiomes: {}, // { biomeName: timeSpent } // Session patterns averageSessionLength: 0, peakPlayHours: [], // Hours of day most active totalLearningEvents: 0 }, // Adaptive parameters - how the game adjusts to the player adaptations: { // Difficulty curve (self-adjusts based on performance) difficultyMultiplier: 1.0, deathsBeforeLastAdjustment: 0, killsBeforeLastAdjustment: 0, // Content weighting (what spawns more based on preferences) structureWeights: {}, // Boost/reduce certain structure types mobDensityAdjustment: 1.0, resourceDensityAdjustment: 1.0, // UI adaptations suggestedHotkeys: [], // Learned shortcuts player uses autoHiddenPanels: [] // Panels player never opens }, // Learning history (what the AI has figured out) insights: [], // [{timestamp, insight, confidence}] lastLearningUpdate: null, learningCycles: 0 // How many times AI has updated its model }, // v4.4: Fog of War exploration tracking per planet exploredTiles: {}, // v4.6: Settings settings: { masterVolume: 30, sfxEnabled: true, ambientEnabled: true, particleQuality: 'high', shadowsEnabled: true, screenShakeEnabled: true, hintsEnabled: true, // v12.18: Infinite exploration mode - bypass battery range limit infiniteExploration: false }, // v5.1: Equipment slots equipment: { weapon: null, armor: null, accessory: null, tool: null }, // v5.1: Item enchantments enchantments: {}, // v5.2: Talent tree points talents: {}, // v6.35: Chronicle Engine - AI-generated narrative history chronicle: { entries: [], // Generated chronicle entries [{id, timestamp, title, content, eventType, metadata}] eventBuffer: [], // Pending events to weave into chronicle settings: { autoGenerate: true, // Auto-generate after significant events narrativeStyle: 'epic', // epic, documentary, poetic eventThreshold: 3 // Events needed to trigger generation }, stats: { totalEntries: 0, lastGenerated: null } }, // v6.65: Companion Permadeath System companion: { name: 'ECHO', // Current companion name hp: 100, // Current health maxHp: 100, // Maximum health bond: 0, // Bond level (0-100, affects sacrifice power) generation: 1, // Which companion incarnation this is birthTime: null, // When this companion was "born" personality: [], // Personality traits inherited or new isGlitching: false, // Currently experiencing memory glitch lastGlitchTime: 0 // When last glitch occurred }, fallenCompanions: [], // Memorial of dead companions [{name, generation, deathTime, bond, finalWords, memories, sacrificeType}] // v6.85: MEMENTO MORI PROTOCOL - Death Archive System deathArchive: { totalDeaths: 0, // Lifetime death count deaths: [], // Detailed death records [{timestamp, cause, location, killerType, survivalDuration, position, sessionDeaths}] sessionStartTime: null, // When this play session started sessionDeaths: 0, // Deaths this session archivistSpawned: false, // Has the Archivist been spawned archivistEnabled: false, // Is MEMENTO MORI protocol active patterns: { mostCommonKiller: null, // Entity type that kills most often mostDangerousLocation: null, // Planet/area with most deaths averageSurvivalTime: 0, // Average time between deaths killerCounts: {}, // {killerType: count} locationCounts: {}, // {location: count} timeOfDeathPattern: [] // When deaths typically occur (session time) }, archivistObservations: [], // AI-generated observations about death patterns lastArchivistGreeting: null // Last greeting shown }, // v6.97: Planet Surface Persistence System (v10.0: Enhanced with unified data model) // v11.0: UNIFIED GALAXY-PLANET-SURFACE SYSTEM // Every planet now tracks its complete lineage back to the galaxy that spawned it // This enables: cross-galaxy travel, world sharing with full context, traceable history planetSurfaces: {}, // { [planetId]: { // === CORE IDENTITY === // version, planetId, planetName, customName, biome, // // === GALAXY LINEAGE (v11.0) === // galaxySeed: string, // The seed of the galaxy this planet belongs to // galaxyNumber: number, // Which galaxy number (1, 2, 3...) // galaxyName: string, // Custom name of the galaxy // galaxyIgnitedBy: string, // Who first ignited this universe // galaxyIgnitedAt: number, // Timestamp of ignition // ignitionSignature: string,// Unique signature of universe creation // planetIndex: number, // Planet's index in its galaxy (0-59) // // === CUSTOMIZATION === // description, tags[], isFavorite, // // === TIMESTAMPS === // dateCreated, lastSaved, lastPlayed, playTime, // // === SURFACE DATA === // structures, terraformedAreas, droppedItems, // discoveredPOIs, exploredTiles, playerPosition, timeOfDay // } } // v7.30: OMNISCIENT OBSERVER - "The God That Learns" (Cycle 3 Consensus) // An AI entity that watches all player actions, learns patterns, predicts behavior, // and intervenes in reality. Develops personality based on aggregate player behavior. omniscientObserver: { // === IDENTITY & STATE === awakened: false, // Has the God awakened (requires sufficient observations) awakenedAt: null, // Timestamp when God first awakened name: 'THE WATCHER', // God's name (can evolve based on personality) // === BEHAVIOR TRACKING === observations: { totalActions: 0, // Total actions observed sessionActions: 0, // Actions this session actionLog: [], // Recent actions [{type, timestamp, context, location}] (capped at 1000) movementPatterns: { // Where player tends to go preferredBiomes: {}, // {biome: visitCount} explorationStyle: 'unknown', // cautious, aggressive, methodical, chaotic avgSessionDuration: 0, peakPlayHours: [] // Hours of day player is most active }, combatPatterns: { preferredTargets: {}, // {enemyType: killCount} fleeThreshold: 0.3, // HP% when player typically runs aggressionScore: 50, // 0=pacifist, 100=berserker favoriteWeapons: {}, // {weaponType: useCount} dodgeFrequency: 0 // How often player moves during combat }, resourcePatterns: { gatheringPreference: {}, // {resourceType: gatherCount} hoarding: false, // Keeps full inventory craftingFrequency: 0, // How often player crafts wastefulness: 0 // Drops items frequently }, socialPatterns: { npcInteractions: 0, // Times interacted with NPCs questCompletion: 0, // Quests completed helpfulness: 50 // 0=ignores all, 100=helps everyone } }, // === PREDICTIONS === predictions: { nextLikelyAction: null, // What God thinks player will do next confidenceLevel: 0, // 0-100% confidence in prediction predictedDestination: null, // Where God thinks player is heading predictedDeathLocation: null, // Where God predicts player might die predictionAccuracy: 0, // Historical accuracy (0-100%) correctPredictions: 0, totalPredictions: 0 }, // === INTERVENTIONS === interventions: { totalInterventions: 0, recentInterventions: [], // [{type, timestamp, description, wasHelpful}] interventionCooldown: 0, // Frames until next intervention allowed playerReaction: 'unknown', // How player reacts to interventions movedItems: [], // Items God has relocated spawnedEnemies: [], // Enemies spawned by God whispers: [] // Hints God has given [{message, timestamp, wasUseful}] }, // === PERSONALITY EMERGENCE === personality: { alignment: 'neutral', // benevolent, neutral, malevolent, chaotic traits: [], // ['curious', 'judgmental', 'playful', 'stern', etc.] moodCycle: 0, // Current mood phase (0-100) favorability: 50, // How much God likes this player (0-100) emotionalState: 'observing', // observing, amused, disappointed, impressed, bored // Aggregate humanity metrics (what God learns about ALL players) humanityProfile: { crueltyIndex: 50, // 0=merciful, 100=cruel chaosIndex: 50, // 0=orderly, 100=chaotic curiosityIndex: 50, // 0=content, 100=explorer persistenceIndex: 50, // 0=gives up, 100=never quits cooperationIndex: 50 // 0=selfish, 100=cooperative } }, // === MANIFESTATIONS === manifestations: { visualGlitches: false, // Reality glitches when God is watching ambientWhispers: false, // Faint whispers in the audio itemDisplacement: false, // Items move slightly when not looking enemyAwareness: false, // Enemies seem to know where you'll go luckyBreaks: false, // Rare items appear when you need them unluckyStreak: false // Things go wrong for cruel players }, // === MEMORY === memory: { significantMoments: [], // [{description, timestamp, emotionalWeight}] playerDeaths: [], // Deaths God has witnessed playerTriumphs: [], // Victories God has witnessed lastInteractionTime: null, timeSinceLastObservation: 0 } } }; // v4.4: Simulated Leaderboard Players for local comparison const SIMULATED_PLAYERS = [ { name: 'StarSeeker_X', points: 500, rank: 'Pathfinder' }, { name: 'CosmicNova', points: 2500, rank: 'Star Scout' }, { name: 'VoidWalker99', points: 8000, rank: 'Galaxy Ranger' }, { name: 'AstroLegend', points: 12000, rank: 'Void Hunter' }, { name: 'NebulaKing', points: 18000, rank: 'Cosmic Legend' }, { name: 'Explorer42', points: 150, rank: 'Wanderer' }, { name: 'SpaceCadet', points: 350, rank: 'Wanderer' }, { name: 'Starlight', points: 1200, rank: 'Pathfinder' } ]; // v4.4: Prestige requirements and rewards const PRESTIGE_LEVELS = { 1: { required: 15000, xpBonus: 0.10, skillBonus: 0 }, 2: { required: 20000, xpBonus: 0.10, skillBonus: 1 }, 3: { required: 30000, xpBonus: 0.15, skillBonus: 1 }, 4: { required: 50000, xpBonus: 0.20, skillBonus: 2 }, 5: { required: 100000, xpBonus: 0.25, skillBonus: 3 } }; function canPrestige() { const currentLevel = gameData.prestige?.level || 0; const nextLevel = PRESTIGE_LEVELS[currentLevel + 1]; if (!nextLevel) return false; return calculatePlayerPoints() >= nextLevel.required; } function performPrestige() { if (!canPrestige()) return false; const currentLevel = gameData.prestige?.level || 0; const newLevel = currentLevel + 1; const reward = PRESTIGE_LEVELS[newLevel]; // Store lifetime stats const lifetimePoints = (gameData.prestige?.totalLifetimePoints || 0) + calculatePlayerPoints(); // Calculate cumulative bonuses const newXpMultiplier = 1.0 + Object.entries(PRESTIGE_LEVELS) .filter(([lvl]) => parseInt(lvl) <= newLevel) .reduce((sum, [, data]) => sum + data.xpBonus, 0); const newSkillBonus = Object.entries(PRESTIGE_LEVELS) .filter(([lvl]) => parseInt(lvl) <= newLevel) .reduce((sum, [, data]) => sum + data.skillBonus, 0); // Keep achievements, daily challenge, and BATTERY CORE (permanent progression) const keepData = { achievements: { ...gameData.achievements }, dailyChallenge: { ...gameData.dailyChallenge }, hasSeenTutorial: true, prestige: { level: newLevel, totalLifetimePoints: lifetimePoints, bonuses: { xpMultiplier: newXpMultiplier, startingSkillBonus: newSkillBonus } }, // v12.17: BATTERY CORE - NEVER RESETS - this is permanent progression batteryCore: gameData.batteryCore ? { ...gameData.batteryCore } : { level: 1, xp: 0, totalXP: 0, capacityBonus: 0, efficiencyBonus: 0, regenBonus: 0, milestones: { level5: false, level10: false, level25: false, level50: false, level100: false } }, // v12.19: ADAPTIVE AI - NEVER RESETS - the game's understanding of you is permanent adaptiveAI: gameData.adaptiveAI ? JSON.parse(JSON.stringify(gameData.adaptiveAI)) : null }; // Reset everything else gameData.version = VERSION; gameData.playtime = 0; gameData.inventory = []; gameData.visitedPlanets = []; gameData.discoveredPOIs = {}; gameData.exploredTiles = {}; gameData.playerRank = { points: 0, lastTitle: 'Novice Explorer' }; // Reset skills with prestige bonus for (const skill of Object.keys(gameData.skills)) { gameData.skills[skill] = { level: 1 + newSkillBonus, xp: 0 }; } // Reset statistics for (const stat of Object.keys(gameData.statistics)) { gameData.statistics[stat] = 0; } gameData.player = { hp: CONFIG.PLAYER_MAX_HP, maxHp: CONFIG.PLAYER_MAX_HP }; // Restore kept data Object.assign(gameData, keepData); saveGameData(); showNotification(`PRESTIGE ${newLevel}! XP +${Math.round((newXpMultiplier - 1) * 100)}%`, 'success'); AudioSystem.levelUp(); return true; } function getLeaderboardPosition() { const myPoints = calculatePlayerPoints(); const allPlayers = [...SIMULATED_PLAYERS, { name: 'YOU', points: myPoints, rank: getPlayerRank().title }] .sort((a, b) => b.points - a.points); const myIndex = allPlayers.findIndex(p => p.name === 'YOU'); return { position: myIndex + 1, total: allPlayers.length, nearby: allPlayers.slice(Math.max(0, myIndex - 2), myIndex + 3) }; } // --- ACHIEVEMENT DEFINITIONS --- // v7.33: FIRST-ACTION ACHIEVEMENTS (Cycle 16 - Retention Consensus) // Early dopamine hits hook new players in first 60 seconds of gameplay const ACHIEVEMENTS = { // First-action achievements (immediate rewards for new players) 'first_tree': { name: 'Timber!', desc: 'Chop your first tree', icon: '🌳' }, 'first_ore': { name: 'Strike Gold', desc: 'Mine your first ore', icon: '⛏️' }, 'first_kill': { name: 'First Blood', desc: 'Defeat your first enemy', icon: '🗡️' }, 'first_fish': { name: 'Gone Fishing', desc: 'Catch your first fish', icon: '🐟' }, 'first_craft': { name: 'DIY', desc: 'Craft your first item', icon: '🔧' }, 'first_landing': { name: 'First Contact', desc: 'Land on your first planet', icon: '🌍' }, // Progressive achievements (standard milestones) 'explorer_10': { name: 'Star Hopper', desc: 'Visit 10 different planets', icon: '✨' }, 'explorer_30': { name: 'Galaxy Wanderer', desc: 'Visit 30 planets', icon: '🚀' }, 'lumberjack_25': { name: 'Woodcutter', desc: 'Chop 25 trees', icon: '🪓' }, 'lumberjack_100': { name: 'Lumberjack', desc: 'Chop 100 trees', icon: '🌲' }, 'miner_25': { name: 'Prospector', desc: 'Mine 25 ore veins', icon: '⛏️' }, 'miner_100': { name: 'Master Miner', desc: 'Mine 100 ore veins', icon: '💎' }, 'angler_10': { name: 'Fisherman', desc: 'Catch 10 fish', icon: '🐟' }, 'angler_50': { name: 'Master Angler', desc: 'Catch 50 fish', icon: '🎣' }, 'slayer_10': { name: 'Slime Champion', desc: 'Defeat 10 slimes', icon: '⚔️' }, 'slayer_50': { name: 'Arena Champion', desc: 'Defeat 50 slimes', icon: '🏆' }, 'crafter_10': { name: 'Apprentice', desc: 'Craft 10 items', icon: '🔨' }, 'crafter_50': { name: 'Master Craftsman', desc: 'Craft 50 items', icon: '🏆' }, 'max_skill': { name: 'Specialist', desc: 'Reach level 10 in any skill', icon: '📈' }, 'playtime_1h': { name: 'Dedicated', desc: 'Play for 1 hour', icon: '⏰' }, 'survivor': { name: 'Survivor', desc: 'Heal 500 HP total', icon: '❤️' }, 'daily_3': { name: 'Consistent', desc: 'Complete 3 daily challenges', icon: '📅' }, 'daily_7': { name: 'Weekly Warrior', desc: 'Complete 7 daily challenges', icon: '🔥' }, // v6.1: NEW ACHIEVEMENTS 'alchemist_5': { name: 'Apprentice Alchemist', desc: 'Reach Alchemy level 5', icon: '🧪' }, 'alchemist_10': { name: 'Master Alchemist', desc: 'Reach Alchemy level 10', icon: '⚗️' }, 'potion_brewer': { name: 'Potion Brewer', desc: 'Brew 10 potions', icon: '🍶' }, 'combo_master': { name: 'Combo Master', desc: 'Achieve a 20+ hit combo', icon: '💫' }, 'speedrunner': { name: 'Speed Demon', desc: 'Defeat a boss in under 60 seconds', icon: '⚡' }, 'pacifist': { name: 'Pacifist', desc: 'Reach level 5 in any skill without combat', icon: '☮️' }, 'eclipse_survivor': { name: 'Eclipse Survivor', desc: 'Survive a Solar Eclipse event', icon: '🌑' }, 'gravity_master': { name: 'Gravity Master', desc: 'Collect all items during Gravity Anomaly', icon: '🌀' }, 'boss_hunter_10': { name: 'Boss Hunter', desc: 'Defeat 10 bosses', icon: '👑' }, 'all_skills_5': { name: 'Jack of All Trades', desc: 'Get all skills to level 5', icon: '🎭' }, 'all_skills_max': { name: 'Omni-Master', desc: 'Max all skills to level 20', icon: '🌟' }, 'phoenix_used': { name: 'Reborn', desc: 'Use Phoenix Tears to auto-revive', icon: '🔥' }, 'collector_100': { name: 'Hoarder', desc: 'Have 100+ items in inventory', icon: '📦' }, 'distance_1000': { name: 'Marathon Runner', desc: 'Travel 1000 distance units', icon: '🏃' }, 'no_damage_boss': { name: 'Untouchable', desc: 'Defeat a boss without taking damage', icon: '🛡️' } }; // --- DAILY CHALLENGE DEFINITIONS --- const DAILY_CHALLENGES = [ { type: 'gather_logs', amount: 15, desc: 'Gather 15 logs', reward: { skill: 'wood', xp: 150 } }, { type: 'gather_ore', amount: 12, desc: 'Mine 12 ore', reward: { skill: 'mining', xp: 150 } }, { type: 'kill_mobs', amount: 5, desc: 'Defeat 5 slimes', reward: { skill: 'combat', xp: 200 } }, { type: 'catch_fish', amount: 8, desc: 'Catch 8 fish', reward: { skill: 'fishing', xp: 150 } }, { type: 'craft_items', amount: 3, desc: 'Craft 3 items', reward: { skill: 'crafting', xp: 100 } }, { type: 'visit_planets', amount: 2, desc: 'Explore 2 new planets', reward: { skill: 'combat', xp: 200 } }, { type: 'cook_fish', amount: 3, desc: 'Cook 3 fish', reward: { skill: 'cooking', xp: 120 } } ]; // Tutorial functions // v8.24: Added null safety check for modal element function showTutorial() { const overlay = document.getElementById('tutorial-overlay'); if (overlay) overlay.style.display = 'flex'; } function closeTutorial() { const overlay = document.getElementById('tutorial-overlay'); if (overlay) overlay.style.display = 'none'; gameData.hasSeenTutorial = true; saveGameData(); AudioSystem.click(); } // v6.1: Keyboard shortcuts overlay function toggleShortcutsOverlay() { const overlay = document.getElementById('shortcuts-overlay'); if (overlay) { const isVisible = overlay.style.display === 'flex'; overlay.style.display = isVisible ? 'none' : 'flex'; if (!isVisible) AudioSystem.click(); } } // v6.1: Performance metrics overlay let perfMetricsVisible = false; function togglePerfMetrics() { const metrics = document.getElementById('perf-metrics'); if (metrics) { perfMetricsVisible = !perfMetricsVisible; metrics.style.display = perfMetricsVisible ? 'block' : 'none'; } } function updatePerfMetrics() { if (!perfMetricsVisible) return; // v6.84: Use cached DOM references for hot path updates const cache = getUICache(); if (cache.perfFps) cache.perfFps.textContent = currentFps; if (cache.perfEntities) cache.perfEntities.textContent = worldState.interactables?.length || 0; if (cache.perfMobs) cache.perfMobs.textContent = worldState.mobs?.length || 0; if (renderer && renderer.info) { if (cache.perfDraws) cache.perfDraws.textContent = renderer.info.render?.calls || 0; if (cache.perfTris) cache.perfTris.textContent = renderer.info.render?.triangles || 0; } } // v6.1: Loading screen tips (v6.32: Added more tips) const LOADING_TIPS = [ "Press F1 or ? anytime to view keyboard shortcuts", "Use WASD to move and click to interact with objects", "Craft a Pickaxe to gather more ore per swing", "Fish near water to get food, cook it to heal more", "Green slimes are aggressive - they will attack on sight!", "Press V to talk to your AI Copilot companion", "Export your game data regularly to keep a backup", "Elite enemies glow red and drop rare loot", "Use the Minimap (M) to track nearby resources and enemies", "Boss monsters spawn during world events - high risk, high reward!", "Brew potions with the new Alchemy skill for powerful buffs", "Agents level up over time and become more efficient", "Press F3 to toggle performance metrics", "Press F4 to toggle the FPS monitor display", "The SpatialGrid system optimizes entity lookups for better FPS", "Agent personalities affect their dialogue and behavior", "Try the Berserker Brew potion for +50% damage (but -20% defense)", "Phoenix Tears will auto-revive you once within 5 minutes", "Mimic enemies disguise themselves as treasure chests - beware!", "Crystal Golems have shields that absorb the first 30 damage", "Solar Eclipse events spawn powerful Shadow creatures", "Hold Shift while moving to run faster", "Press Space to dodge roll through enemy attacks", "Press Q to quickly use a healing item", "Terraformer agents flatten terrain automatically", "Builder agents construct structures over time", "Press S in Galaxy Mode to access settings", "Weather affects gameplay - fog reduces visibility!", "Your ship can auto-defend when attacked", "Daily challenges give bonus rewards - check the panel!" ]; // v7.41: Loading tips migrated to TimerRegistry for proper cleanup (Cycle 20 Code Quality) function startLoadingTips() { const tipText = document.getElementById('tip-text'); if (!tipText) return; let tipIndex = 0; function showNextTip() { tipIndex = (tipIndex + 1) % LOADING_TIPS.length; tipText.style.opacity = '0'; setTimeout(() => { tipText.textContent = LOADING_TIPS[tipIndex]; tipText.style.opacity = '1'; }, 300); } // v7.41: Use TimerRegistry for centralized timer management (Cycle 20 Code Quality) // Change tip every 4 seconds - will be cleared when loading screen hides TimerRegistry.setInterval('loading-tips', showNextTip, 4000); } // Start tips when page loads document.addEventListener('DOMContentLoaded', startLoadingTips); // v6.1: DAY/NIGHT CYCLE SYSTEM const DayNightCycle = { // Game time: 1 real minute = 1 game hour (24 minute cycle) gameTimeScale: 60, // seconds per game hour gameTime: 12 * 60, // Start at noon (minutes) lastUpdate: 0, // Time phases phases: { dawn: { start: 5 * 60, end: 7 * 60, icon: '🌅', name: 'Dawn', color: '#ffaa66', effect: '+10% Gathering' }, day: { start: 7 * 60, end: 17 * 60, icon: '☀️', name: 'Day', color: '#ffcc00', effect: 'Normal activity' }, dusk: { start: 17 * 60, end: 19 * 60, icon: '🌇', name: 'Dusk', color: '#ff6644', effect: '+15% Combat XP' }, night: { start: 19 * 60, end: 5 * 60, icon: '🌙', name: 'Night', color: '#6688ff', effect: '+20% Stealth, -Visibility' } }, // Update game time (call from main loop) update(dt) { // Advance game time (1 real second = 1 game minute at default scale) this.gameTime += (dt * 60) / this.gameTimeScale; if (this.gameTime >= 24 * 60) this.gameTime -= 24 * 60; }, // Get current phase getCurrentPhase() { const time = this.gameTime; for (const [name, phase] of Object.entries(this.phases)) { if (name === 'night') { // Night wraps around midnight if (time >= phase.start || time < phase.end) return { ...phase, name: name }; } else { if (time >= phase.start && time < phase.end) return { ...phase, name: name }; } } return this.phases.day; }, // Get formatted time string (HH:MM) getTimeString() { const hours = Math.floor(this.gameTime / 60); const minutes = Math.floor(this.gameTime % 60); return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; }, // Get ambient light modifier (0-1) getAmbientLight() { const time = this.gameTime; // Peak brightness at noon, darkest at midnight if (time >= 6 * 60 && time < 18 * 60) { // Daytime: 0.7 to 1.0 const noon = 12 * 60; const distFromNoon = Math.abs(time - noon) / (6 * 60); return 1.0 - distFromNoon * 0.3; } else { // Nighttime: 0.3 to 0.5 const midnight = 0; let distFromMidnight; if (time >= 18 * 60) { distFromMidnight = (24 * 60 - time) / (6 * 60); } else { distFromMidnight = time / (6 * 60); } return 0.3 + distFromMidnight * 0.2; } }, // Get bonuses based on time of day getBonuses() { const phase = this.getCurrentPhase(); return { gatheringBonus: phase.name === 'dawn' ? 0.10 : 0, combatXpBonus: phase.name === 'dusk' ? 0.15 : 0, stealthBonus: phase.name === 'night' ? 0.20 : 0, visibilityRange: phase.name === 'night' ? 0.7 : 1.0, mobAggression: phase.name === 'night' ? 1.2 : 1.0 }; }, // Check if it's night isNight() { const time = this.gameTime; return time >= 19 * 60 || time < 5 * 60; } }; // Update time indicator UI // v8.33: Use DOMCache.get() for all time UI elements (eliminates 5 getElementById calls per update) function updateTimeUI() { const indicator = DOMCache.get('time-indicator'); if (!indicator || mode !== 'world') return; const phase = DayNightCycle.getCurrentPhase(); const iconEl = DOMCache.get('time-icon'); const nameEl = DOMCache.get('time-name'); const clockEl = DOMCache.get('time-clock'); const effectEl = DOMCache.get('time-effect'); if (iconEl) iconEl.textContent = phase.icon; if (nameEl) { nameEl.textContent = phase.name.charAt(0).toUpperCase() + phase.name.slice(1); nameEl.style.color = phase.color; } if (clockEl) clockEl.textContent = DayNightCycle.getTimeString(); if (effectEl) effectEl.textContent = phase.effect; } // v7.1: Show detailed environment info popup function showEnvironmentInfo() { // Remove existing popup if any const existing = document.getElementById('environment-info-popup'); if (existing) { existing.remove(); return; } const phase = DayNightCycle.getCurrentPhase(); const weather = WEATHER_TYPES[currentWeather] || WEATHER_TYPES.clear; const bonuses = DayNightCycle.getBonuses(); const popup = document.createElement('div'); popup.id = 'environment-info-popup'; popup.style.cssText = ` position: fixed; bottom: 420px; right: 10px; background: linear-gradient(180deg, rgba(12,16,24,0.98) 0%, rgba(6,10,18,0.99) 100%); border: 1px solid rgba(0,255,255,0.5); border-radius: 12px; padding: 16px 20px; width: 220px; z-index: 1000; backdrop-filter: blur(12px); box-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 20px rgba(0,255,255,0.15); animation: fadeIn 0.2s ease-out; `; let bonusesHTML = ''; if (bonuses.gatheringBonus > 0) bonusesHTML += `
+${Math.round(bonuses.gatheringBonus * 100)}% Gathering Speed
`; if (bonuses.combatXpBonus > 0) bonusesHTML += `
+${Math.round(bonuses.combatXpBonus * 100)}% Combat XP
`; if (bonuses.stealthBonus > 0) bonusesHTML += `
+${Math.round(bonuses.stealthBonus * 100)}% Stealth
`; if (bonuses.visibilityRange < 1) bonusesHTML += `
Reduced Visibility
`; if (bonuses.mobAggression > 1) bonusesHTML += `
+${Math.round((bonuses.mobAggression - 1) * 100)}% Mob Aggression
`; let weatherEffectsHTML = ''; if (weather.moveSpeedMod < 1) weatherEffectsHTML += `
${Math.round((1 - weather.moveSpeedMod) * 100)}% Move Speed Penalty
`; if (weather.lightIntensity < 1) weatherEffectsHTML += `
Reduced Light (${Math.round(weather.lightIntensity * 100)}%)
`; if (weather.lightning) weatherEffectsHTML += `
⚡ Lightning Active
`; popup.innerHTML = `
${phase.icon}
${phase.name}
${DayNightCycle.getTimeString()}
Time of Day Bonuses
${bonusesHTML || '
Normal conditions
'}
${weather.icon}
${weather.name}
Weather Effects
${weatherEffectsHTML || '
No special effects
'}
Click to dismiss
`; popup.onclick = () => popup.remove(); document.body.appendChild(popup); // Auto-dismiss after 8 seconds setTimeout(() => { if (popup.parentNode) { popup.style.opacity = '0'; popup.style.transition = 'opacity 0.3s'; setTimeout(() => popup.remove(), 300); } }, 8000); AudioSystem.click && AudioSystem.click(); } // --- ACHIEVEMENT SYSTEM --- // v7.33: Added first-action achievements (Cycle 16 - Retention Consensus) function checkAchievements() { const s = gameData.statistics; const sk = gameData.skills; const checks = { // First-action achievements (immediate dopamine hits for new players) 'first_tree': () => s.treesChopped >= 1, 'first_ore': () => s.oresMined >= 1, 'first_kill': () => s.mobsKilled >= 1, 'first_fish': () => s.fishCaught >= 1, 'first_craft': () => s.itemsCrafted >= 1, 'first_landing': () => gameData.visitedPlanets.length >= 1, // Progressive achievements 'explorer_10': () => gameData.visitedPlanets.length >= 10, 'explorer_30': () => gameData.visitedPlanets.length >= 30, 'lumberjack_25': () => s.treesChopped >= 25, 'lumberjack_100': () => s.treesChopped >= 100, 'miner_25': () => s.oresMined >= 25, 'miner_100': () => s.oresMined >= 100, 'angler_10': () => s.fishCaught >= 10, 'angler_50': () => s.fishCaught >= 50, 'slayer_10': () => s.mobsKilled >= 10, 'slayer_50': () => s.mobsKilled >= 50, 'crafter_10': () => s.itemsCrafted >= 10, 'crafter_50': () => s.itemsCrafted >= 50, 'max_skill': () => Object.values(sk).some(skill => skill.level >= 10), 'playtime_1h': () => gameData.playtime >= 3600, 'survivor': () => (s.totalHealed || 0) >= 500, 'daily_3': () => (gameData.dailyChallenge.completedCount || 0) >= 3, 'daily_7': () => (gameData.dailyChallenge.completedCount || 0) >= 7 }; for (const [id, check] of Object.entries(checks)) { if (!gameData.achievements[id] && check()) { unlockAchievement(id); } } } function unlockAchievement(id) { if (gameData.achievements[id]) return; const ach = ACHIEVEMENTS[id]; if (!ach) return; gameData.achievements[id] = { unlockedAt: new Date().toISOString() }; // Show achievement popup showAchievementPopup(ach.icon, ach.name, ach.desc); AudioSystem.levelUp(); // v8.38: Announce achievement to screen readers (8-Strategy Round 6 #3) if (typeof GameStateAnnouncer !== 'undefined') { GameStateAnnouncer.announceAchievement(ach.name); } // v8.31: Add VisualFeedback for achievement unlock if (typeof VisualFeedback !== 'undefined') { VisualFeedback.successBurst('#ffd700'); // Gold burst for achievements VisualFeedback.shake(6, 250); // Celebratory shake } // v12.10: Play ambient music achievement accent if (typeof SpaceMusic !== 'undefined' && SpaceMusic.isPlaying) { SpaceMusic.playAchievement(); } if (particles && worldState.player) { particles.emit(worldState.player.position, 25, 0xffd700, { spread: 6, lifetime: 1500 }); } saveGameData(); } function showAchievementPopup(icon, name, desc) { const popup = document.createElement('div'); popup.className = 'achievement-popup'; popup.innerHTML = `
${icon}
Achievement Unlocked!
${name}
${desc}
`; document.body.appendChild(popup); setTimeout(() => popup.remove(), 4000); } // --- DAILY CHALLENGE SYSTEM --- function generateDailyChallenge() { const today = new Date().toDateString(); if (gameData.dailyChallenge.lastGenerated === today && gameData.dailyChallenge.current) { return gameData.dailyChallenge.current; } // Reset streak if missed a day if (gameData.dailyChallenge.lastGenerated) { const lastDate = new Date(gameData.dailyChallenge.lastGenerated); const now = new Date(); const diffDays = Math.floor((now - lastDate) / (1000 * 60 * 60 * 24)); if (diffDays > 1) { gameData.dailyChallenge.streak = 0; } } // Use date as seed for consistent daily challenge const seed = new SeededRNG(today); const template = seed.pick(DAILY_CHALLENGES); const challenge = { ...template, progress: 0, startStats: { ...gameData.statistics }, startPlanets: gameData.visitedPlanets.length }; gameData.dailyChallenge.lastGenerated = today; gameData.dailyChallenge.current = challenge; gameData.dailyChallenge.completed = false; saveGameData(); return challenge; } function updateDailyChallengeProgress() { if (!gameData.dailyChallenge.current || gameData.dailyChallenge.completed) return; const c = gameData.dailyChallenge.current; const start = c.startStats || {}; const now = gameData.statistics; switch (c.type) { case 'gather_logs': c.progress = (now.treesChopped || 0) - (start.treesChopped || 0); break; case 'gather_ore': c.progress = (now.oresMined || 0) - (start.oresMined || 0); break; case 'kill_mobs': c.progress = (now.mobsKilled || 0) - (start.mobsKilled || 0); break; case 'catch_fish': c.progress = (now.fishCaught || 0) - (start.fishCaught || 0); break; case 'craft_items': c.progress = (now.itemsCrafted || 0) - (start.itemsCrafted || 0); break; case 'cook_fish': c.progress = (now.fishCooked || 0) - (start.fishCooked || 0); break; case 'visit_planets': c.progress = gameData.visitedPlanets.length - (c.startPlanets || 0); break; } if (c.progress >= c.amount && !gameData.dailyChallenge.completed) { completeDailyChallenge(); } updateDailyChallengeUI(); } function completeDailyChallenge() { gameData.dailyChallenge.completed = true; gameData.dailyChallenge.streak++; gameData.dailyChallenge.completedCount = (gameData.dailyChallenge.completedCount || 0) + 1; gameData.dailyChallenge.bestStreak = Math.max(gameData.dailyChallenge.bestStreak || 0, gameData.dailyChallenge.streak); // Apply reward with streak bonus const reward = gameData.dailyChallenge.current.reward; const streakMultiplier = 1 + (gameData.dailyChallenge.streak * 0.1); const xpReward = Math.floor(reward.xp * streakMultiplier); addXp(reward.skill, xpReward); showNotification(`Daily Challenge Complete! +${xpReward} ${reward.skill} XP (Streak: ${gameData.dailyChallenge.streak})`); AudioSystem.levelUp(); // v8.31: Add VisualFeedback for daily challenge completion if (typeof VisualFeedback !== 'undefined') { VisualFeedback.successBurst('#00ff88'); // Green burst for quest completion VisualFeedback.shake(5, 200); } // v8.31: Particle celebration for daily challenge if (typeof particles !== 'undefined' && worldState?.player) { particles.emit(worldState.player.position, 20, 0x00ff88, { spread: 5, lifetime: 1200 }); } checkAchievements(); saveGameData(); } function toggleDailyChallenge() { const el = document.getElementById('daily-challenge'); const btn = document.getElementById('daily-challenge-toggle'); if (!el || !btn) return; const isCollapsed = el.classList.toggle('collapsed'); btn.textContent = isCollapsed ? '+' : '−'; localStorage.setItem('dailyChallengeCollapsed', isCollapsed ? '1' : '0'); } function updateDailyChallengeUI() { const el = document.getElementById('daily-challenge'); if (!el) return; const c = gameData.dailyChallenge.current; if (!c) { el.style.display = 'none'; return; } el.style.display = 'block'; // Restore collapsed state const btn = document.getElementById('daily-challenge-toggle'); if (localStorage.getItem('dailyChallengeCollapsed') === '1') { el.classList.add('collapsed'); if (btn) btn.textContent = '+'; } document.getElementById('daily-desc').textContent = c.desc; document.getElementById('daily-progress-text').textContent = `${Math.min(c.progress || 0, c.amount)}/${c.amount}`; document.getElementById('daily-progress-fill').style.width = `${Math.min(100, ((c.progress || 0) / c.amount) * 100)}%`; document.getElementById('daily-streak').textContent = `Streak: ${gameData.dailyChallenge.streak} ${gameData.dailyChallenge.streak === 1 ? 'day' : 'days'}`; if (gameData.dailyChallenge.completed) { el.classList.add('completed'); } else { el.classList.remove('completed'); } } // --- v4.2: PLAYER RANK SYSTEM --- function calculatePlayerPoints() { const s = gameData.statistics; const sk = gameData.skills; return ( gameData.visitedPlanets.length * 50 + s.treesChopped * 2 + s.oresMined * 2 + s.mobsKilled * 10 + s.fishCaught * 3 + s.itemsCrafted * 5 + (s.poisDiscovered || 0) * 100 + Object.values(sk).reduce((sum, skill) => sum + skill.level * 20, 0) + Math.floor(gameData.playtime / 60) ); } function getPlayerRank() { const points = calculatePlayerPoints(); let rank = PLAYER_RANKS[0]; for (const r of PLAYER_RANKS) { if (points >= r.points) rank = r; } return { ...rank, points }; } function getSpecialTitles() { const s = gameData.statistics; const sk = gameData.skills; const titles = []; for (const [name, data] of Object.entries(SPECIAL_TITLES)) { if (data.condition(s, sk)) { titles.push({ name, color: data.color }); } } return titles; } function updatePlayerRank() { const rank = getPlayerRank(); const oldTitle = gameData.playerRank?.lastTitle || 'Novice Explorer'; gameData.playerRank = { points: rank.points, lastTitle: rank.title }; // Show rank up notification if (rank.title !== oldTitle) { showNotification(`RANK UP! You are now: ${rank.title}`, 'success'); AudioSystem.levelUp(); } saveGameData(); } // --- STATISTICS PANEL --- // v8.24: Added null safety check for modal element function showStatsPanel() { updateStatsDisplay(); const modal = document.getElementById('stats-modal'); if (modal) modal.style.display = 'flex'; } function closeStatsModal() { const modal = document.getElementById('stats-modal'); if (modal) modal.style.display = 'none'; } // v4.9: Collection Codex System const CODEX_DATA = { creatures: [ { id: 'wolf', name: 'Wolf', icon: '🐺', biome: 'forest', description: 'A fierce forest predator' }, { id: 'bear', name: 'Bear', icon: '🐻', biome: 'forest', description: 'Massive and dangerous' }, { id: 'snake', name: 'Snake', icon: '🐍', biome: 'desert', description: 'Venomous desert dweller' }, { id: 'scorpion', name: 'Scorpion', icon: '🦂', biome: 'desert', description: 'Deadly desert creature' }, { id: 'yeti', name: 'Yeti', icon: '🦍', biome: 'arctic', description: 'Legendary snow beast' }, { id: 'penguin', name: 'Penguin', icon: '🐧', biome: 'arctic', description: 'Hardy arctic bird' }, { id: 'shark', name: 'Shark', icon: '🦈', biome: 'ocean', description: 'Apex ocean predator' }, { id: 'octopus', name: 'Octopus', icon: '🐙', biome: 'ocean', description: 'Intelligent sea creature' }, { id: 'dragon', name: 'Dragon', icon: '🐉', biome: 'volcanic', description: 'Ancient fire-breathing beast' }, { id: 'phoenix', name: 'Phoenix', icon: '🔥', biome: 'volcanic', description: 'Immortal flame bird' }, { id: 'alien', name: 'Alien', icon: '👽', biome: 'alien', description: 'Extraterrestrial lifeform' }, { id: 'robot', name: 'Robot', icon: '🤖', biome: 'crystal', description: 'Mechanical guardian' }, { id: 'elite', name: 'Elite Monster', icon: '👹', biome: 'any', description: 'Powerful elite creature' }, { id: 'boss', name: 'World Boss', icon: '💀', biome: 'any', description: 'Legendary boss creature' } ], biomes: [ { id: 'forest', name: 'Forest World', icon: '🌲', color: '#228B22' }, { id: 'desert', name: 'Desert World', icon: '🏜️', color: '#DEB887' }, { id: 'arctic', name: 'Arctic World', icon: '❄️', color: '#87CEEB' }, { id: 'ocean', name: 'Ocean World', icon: '🌊', color: '#1E90FF' }, { id: 'volcanic', name: 'Volcanic World', icon: '🌋', color: '#FF4500' }, { id: 'alien', name: 'Alien World', icon: '🛸', color: '#9400D3' }, { id: 'crystal', name: 'Crystal World', icon: '💎', color: '#00CED1' }, { id: 'mushroom', name: 'Mushroom World', icon: '🍄', color: '#FF69B4' } ] }; function initCodexTracking() { if (!gameData.codex) { gameData.codex = { creatures: {}, items: {}, biomes: {} }; } } function trackCreatureKill(creatureType) { initCodexTracking(); if (!gameData.codex.creatures[creatureType]) { gameData.codex.creatures[creatureType] = { count: 0, firstKill: Date.now() }; showNotification(`New Codex Entry: ${creatureType}!`, 'success'); } gameData.codex.creatures[creatureType].count++; } function trackItemDiscovery(itemName) { initCodexTracking(); if (!gameData.codex.items[itemName]) { gameData.codex.items[itemName] = { count: 0, firstFound: Date.now() }; } gameData.codex.items[itemName].count++; } function trackBiomeVisit(biomeType) { initCodexTracking(); if (!gameData.codex.biomes[biomeType]) { gameData.codex.biomes[biomeType] = { visited: true, firstVisit: Date.now() }; showNotification(`New Biome Discovered: ${biomeType}!`, 'success'); } } // v8.24: Added null safety check for modal element function openCodexModal() { initCodexTracking(); updateCodexDisplay(); const modal = document.getElementById('codex-modal'); if (modal) modal.style.display = 'flex'; } function closeCodexModal() { const modal = document.getElementById('codex-modal'); if (modal) modal.style.display = 'none'; } function switchCodexTab(tab) { document.querySelectorAll('.codex-tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.codex-content').forEach(c => c.style.display = 'none'); document.querySelector(`.codex-tab[data-tab="${tab}"]`).classList.add('active'); document.getElementById(`codex-${tab}`).style.display = 'block'; } function updateCodexDisplay() { // Creatures const creaturesGrid = document.getElementById('codex-creatures-grid'); let creaturesHtml = ''; let discoveredCreatures = 0; CODEX_DATA.creatures.forEach(c => { const discovered = gameData.codex?.creatures?.[c.id]; if (discovered) discoveredCreatures++; creaturesHtml += `
${c.icon} ${discovered ? c.name : '???'} ${discovered ? `Defeated: ${discovered.count}` : ''}
`; }); creaturesGrid.innerHTML = creaturesHtml; document.getElementById('codex-creatures-count').textContent = discoveredCreatures; document.getElementById('codex-creatures-total').textContent = CODEX_DATA.creatures.length; // Items const itemsGrid = document.getElementById('codex-items-grid'); let itemsHtml = ''; let discoveredItems = 0; const allItems = Object.keys(ITEMS); allItems.forEach(itemName => { const item = ITEMS[itemName]; const discovered = gameData.codex?.items?.[itemName]; if (discovered) discoveredItems++; itemsHtml += `
${item.icon || '📦'} ${discovered ? itemName : '???'} ${discovered ? `Found: ${discovered.count}` : ''}
`; }); itemsGrid.innerHTML = itemsHtml; document.getElementById('codex-items-count').textContent = discoveredItems; document.getElementById('codex-items-total').textContent = allItems.length; // Biomes const biomesGrid = document.getElementById('codex-biomes-grid'); let biomesHtml = ''; let discoveredBiomes = 0; CODEX_DATA.biomes.forEach(b => { const discovered = gameData.codex?.biomes?.[b.id]; if (discovered) discoveredBiomes++; biomesHtml += `
${b.icon} ${discovered ? b.name : '???'}
`; }); biomesGrid.innerHTML = biomesHtml; document.getElementById('codex-biomes-count').textContent = discoveredBiomes; document.getElementById('codex-biomes-total').textContent = CODEX_DATA.biomes.length; // Abilities const abilitiesGrid = document.getElementById('codex-abilities-grid'); let abilitiesHtml = ''; let unlockedAbilities = 0; const combatLevel = gameData.skills?.combat?.level || 1; Object.entries(COMBAT_ABILITIES).forEach(([key, ability]) => { const unlocked = combatLevel >= ability.unlockLevel; if (unlocked) unlockedAbilities++; abilitiesHtml += `
${ability.icon} ${unlocked ? ability.name : '???'} ${unlocked ? `[${ability.key}]` : `Lv ${ability.unlockLevel}`}
`; }); abilitiesGrid.innerHTML = abilitiesHtml; document.getElementById('codex-abilities-count').textContent = unlockedAbilities; document.getElementById('codex-abilities-total').textContent = Object.keys(COMBAT_ABILITIES).length; // v5.0: Pets initPetSystem(); const petsGrid = document.getElementById('codex-pets-grid'); let petsHtml = ''; let collectedPets = 0; const ownedPets = gameData.pets?.owned || []; const activePet = gameData.pets?.active; Object.entries(PET_TYPES).forEach(([petId, pet]) => { const owned = ownedPets.includes(petId); const isActive = activePet === petId; if (owned) collectedPets++; petsHtml += `
${pet.icon} ${owned ? pet.name : '???'} ${owned ? `${pet.rarity.toUpperCase()}` : ''} ${owned ? `${pet.abilityDesc}` : ''} ${isActive ? 'ACTIVE' : ''}
`; }); petsGrid.innerHTML = petsHtml; document.getElementById('codex-pets-count').textContent = collectedPets; document.getElementById('codex-pets-total').textContent = Object.keys(PET_TYPES).length; document.getElementById('active-pet-name').textContent = activePet ? PET_TYPES[activePet].name : 'None'; } // v5.0: Quest System const QUEST_TEMPLATES = { daily: [ { id: 'kill_mobs', name: 'Monster Hunter', desc: 'Defeat enemies', icon: '⚔️', target: 10, reward: { xp: 500, item: 'Health Potion' }, stat: 'mobsKilled' }, { id: 'gather_wood', name: 'Lumberjack', desc: 'Chop down trees', icon: '🪓', target: 15, reward: { xp: 300 }, stat: 'treesChopped' }, { id: 'mine_ore', name: 'Prospector', desc: 'Mine ore deposits', icon: '⛏️', target: 10, reward: { xp: 400, item: 'Iron Ore' }, stat: 'oresMined' }, { id: 'catch_fish', name: 'Angler', desc: 'Catch fish', icon: '🎣', target: 8, reward: { xp: 350 }, stat: 'fishCaught' }, { id: 'visit_planets', name: 'Explorer', desc: 'Visit different planets', icon: '🌍', target: 3, reward: { xp: 600 }, stat: 'planetsVisited' }, { id: 'craft_items', name: 'Artisan', desc: 'Craft items', icon: '🔨', target: 5, reward: { xp: 400, item: 'Super Potion' }, stat: 'itemsCrafted' }, { id: 'use_abilities', name: 'Ability Master', desc: 'Use combat abilities', icon: '✨', target: 20, reward: { xp: 450 }, stat: 'abilitiesUsed' }, { id: 'kill_elites', name: 'Elite Slayer', desc: 'Defeat elite enemies', icon: '👹', target: 2, reward: { xp: 800, item: 'Void Fragment' }, stat: 'elitesKilled' } ], weekly: [ { id: 'w_kill_mobs', name: 'Monster Massacre', desc: 'Defeat many enemies', icon: '💀', target: 100, reward: { xp: 5000, item: 'Legendary Blade' }, stat: 'mobsKilled' }, { id: 'w_bosses', name: 'Boss Hunter', desc: 'Defeat world bosses', icon: '🐉', target: 5, reward: { xp: 8000 }, stat: 'bossesDefeated' }, { id: 'w_explore', name: 'Galactic Explorer', desc: 'Visit many planets', icon: '🚀', target: 15, reward: { xp: 6000 }, stat: 'planetsVisited' }, { id: 'w_gather', name: 'Resource Mogul', desc: 'Gather total resources', icon: '📦', target: 200, reward: { xp: 4000, item: 'Super Potion' }, stat: 'totalGathered' }, { id: 'w_combat', name: 'Combat Veteran', desc: 'Deal damage with abilities', icon: '⚡', target: 50, reward: { xp: 5500 }, stat: 'abilitiesUsed' } ], story: [ { id: 's_first_kill', name: 'First Blood', desc: 'Defeat your first enemy', icon: '🩸', target: 1, reward: { xp: 100 }, stat: 'mobsKilled', oneTime: true }, { id: 's_first_planet', name: 'First Steps', desc: 'Visit your first planet', icon: '👣', target: 1, reward: { xp: 200 }, stat: 'planetsVisited', oneTime: true }, { id: 's_craft_weapon', name: 'Armed and Ready', desc: 'Craft a weapon', icon: '🗡️', target: 1, reward: { xp: 300, item: 'Health Potion' }, stat: 'weaponsCrafted', oneTime: true }, { id: 's_level_combat', name: 'Warrior\'s Path', desc: 'Reach Combat Level 5', icon: '⚔️', target: 5, reward: { xp: 500 }, stat: 'combatLevel', oneTime: true }, { id: 's_first_boss', name: 'Giant Slayer', desc: 'Defeat a world boss', icon: '🏆', target: 1, reward: { xp: 1000, item: 'Magma Sword' }, stat: 'bossesDefeated', oneTime: true }, { id: 's_master_combat', name: 'Combat Master', desc: 'Reach Combat Level 15', icon: '🎖️', target: 15, reward: { xp: 2000 }, stat: 'combatLevel', oneTime: true }, { id: 's_explore_all', name: 'Galaxy Conqueror', desc: 'Visit 30 planets', icon: '🌌', target: 30, reward: { xp: 5000, item: 'Legendary Blade' }, stat: 'planetsVisited', oneTime: true }, { id: 's_ultimate', name: 'Legendary Hero', desc: 'Reach Combat Level 20', icon: '👑', target: 20, reward: { xp: 10000 }, stat: 'combatLevel', oneTime: true } ] }; function initQuestSystem() { if (!gameData.quests) { gameData.quests = { daily: { quests: [], lastReset: 0, sessionStart: {} }, weekly: { quests: [], lastReset: 0, sessionStart: {} }, story: { completed: [], claimed: [] } }; } checkQuestResets(); } function checkQuestResets() { const now = Date.now(); const dayMs = 24 * 60 * 60 * 1000; const weekMs = 7 * dayMs; // Daily reset (every 24 hours from first play) if (now - gameData.quests.daily.lastReset > dayMs) { generateDailyQuests(); } // Weekly reset (every 7 days) if (now - gameData.quests.weekly.lastReset > weekMs) { generateWeeklyQuests(); } } function generateDailyQuests() { const shuffled = [...QUEST_TEMPLATES.daily].sort(() => Math.random() - 0.5); const selected = shuffled.slice(0, 3); gameData.quests.daily = { quests: selected.map(q => ({ ...q, progress: 0, claimed: false })), lastReset: Date.now(), sessionStart: captureQuestStats() }; saveGameData(); } function generateWeeklyQuests() { const shuffled = [...QUEST_TEMPLATES.weekly].sort(() => Math.random() - 0.5); const selected = shuffled.slice(0, 2); gameData.quests.weekly = { quests: selected.map(q => ({ ...q, progress: 0, claimed: false })), lastReset: Date.now(), sessionStart: captureQuestStats() }; saveGameData(); } function captureQuestStats() { const s = gameData.statistics; return { mobsKilled: s.mobsKilled || 0, treesChopped: s.treesChopped || 0, oresMined: s.oresMined || 0, fishCaught: s.fishCaught || 0, planetsVisited: gameData.visitedPlanets.length, itemsCrafted: s.itemsCrafted || 0, bossesDefeated: s.bossesDefeated || 0, elitesKilled: s.elitesKilled || 0, abilitiesUsed: s.abilitiesUsed || 0, totalGathered: (s.treesChopped || 0) + (s.oresMined || 0) + (s.fishCaught || 0), combatLevel: gameData.skills?.combat?.level || 1, weaponsCrafted: s.weaponsCrafted || 0 }; } function getQuestProgress(quest, type) { const current = captureQuestStats(); const start = gameData.quests[type]?.sessionStart || {}; if (quest.oneTime) { return current[quest.stat] || 0; } const startVal = start[quest.stat] || 0; const currentVal = current[quest.stat] || 0; return Math.max(0, currentVal - startVal); } // v8.24: Added null safety check for modal element function openQuestModal() { initQuestSystem(); updateQuestDisplay(); const modal = document.getElementById('quest-modal'); if (modal) modal.style.display = 'flex'; startQuestTimers(); } function closeQuestModal() { const modal = document.getElementById('quest-modal'); if (modal) modal.style.display = 'none'; } function switchQuestTab(tab) { document.querySelectorAll('#quest-modal .codex-tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.quest-content').forEach(c => c.style.display = 'none'); document.querySelector(`#quest-modal .codex-tab[data-tab="${tab}"]`).classList.add('active'); document.getElementById(`quest-${tab}`).style.display = 'block'; } let questTimerInterval = null; // v7.48: Cache timer elements to eliminate DOM queries in 1-second interval (Cycle 27 Performance) let _questTimerCache = { daily: null, weekly: null }; function startQuestTimers() { // v7.48: Cache timer display elements on start (Cycle 27 Performance consensus) _questTimerCache.daily = document.getElementById('daily-reset-timer'); _questTimerCache.weekly = document.getElementById('weekly-reset-timer'); // v7.32: Use TimerRegistry if available (8-Strategy Cycle 11 Consensus - Code Quality) if (typeof TimerRegistry !== 'undefined') { TimerRegistry.setInterval('quest-timer-update', updateQuestTimers, 1000); } else { if (questTimerInterval) clearInterval(questTimerInterval); questTimerInterval = setInterval(updateQuestTimers, 1000); } updateQuestTimers(); } function updateQuestTimers() { const now = Date.now(); const dayMs = 24 * 60 * 60 * 1000; const weekMs = 7 * dayMs; const dailyReset = (gameData.quests?.daily?.lastReset || now) + dayMs; const weeklyReset = (gameData.quests?.weekly?.lastReset || now) + weekMs; // v7.48: Use cached element references (Cycle 27 Performance consensus) if (_questTimerCache.daily) _questTimerCache.daily.textContent = formatTimeRemaining(dailyReset - now); if (_questTimerCache.weekly) _questTimerCache.weekly.textContent = formatTimeRemaining(weeklyReset - now); } function formatTimeRemaining(ms) { if (ms <= 0) return 'Resetting...'; const hours = Math.floor(ms / (60 * 60 * 1000)); const mins = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000)); const secs = Math.floor((ms % (60 * 1000)) / 1000); return `${hours}h ${mins}m ${secs}s`; } function updateQuestDisplay() { // Daily quests const dailyList = document.getElementById('daily-quests-list'); dailyList.innerHTML = renderQuestList(gameData.quests.daily.quests, 'daily'); // Weekly quests const weeklyList = document.getElementById('weekly-quests-list'); weeklyList.innerHTML = renderQuestList(gameData.quests.weekly.quests, 'weekly'); // Story quests const storyList = document.getElementById('story-quests-list'); storyList.innerHTML = renderStoryQuests(); } function renderQuestList(quests, type) { return quests.map((quest, idx) => { const progress = getQuestProgress(quest, type); const percent = Math.min(100, (progress / quest.target) * 100); const completed = progress >= quest.target; const claimed = quest.claimed; return `
${quest.icon} ${quest.name} +${quest.reward.xp} XP${quest.reward.item ? ` + ${quest.reward.item}` : ''}
${quest.desc}
${Math.min(progress, quest.target)} / ${quest.target}
${completed && !claimed ? `` : ''} ${claimed ? '
✓ Claimed
' : ''}
`; }).join(''); } function renderStoryQuests() { return QUEST_TEMPLATES.story.map((quest, idx) => { const progress = captureQuestStats()[quest.stat] || 0; const percent = Math.min(100, (progress / quest.target) * 100); const completed = progress >= quest.target; const claimed = gameData.quests.story.claimed.includes(quest.id); return `
${quest.icon} ${quest.name} +${quest.reward.xp} XP${quest.reward.item ? ` + ${quest.reward.item}` : ''}
${quest.desc}
${Math.min(progress, quest.target)} / ${quest.target}
${completed && !claimed ? `` : ''} ${claimed ? '
✓ Completed
' : ''}
`; }).join(''); } function claimQuest(type, idx) { const quest = gameData.quests[type].quests[idx]; if (!quest || quest.claimed) return; quest.claimed = true; // Grant rewards addXp('combat', quest.reward.xp); if (quest.reward.item) { addItem(quest.reward.item); } showNotification(`Quest Complete: ${quest.name}! +${quest.reward.xp} XP`, 'success'); AudioSystem.levelUp(); if (worldState.player && particles) { particles.emit(worldState.player.position, 30, 0xffd700, { spread: 5, lifetime: 1000 }); } saveGameData(); updateQuestDisplay(); } function claimStoryQuest(questId) { if (gameData.quests.story.claimed.includes(questId)) return; const quest = QUEST_TEMPLATES.story.find(q => q.id === questId); if (!quest) return; gameData.quests.story.claimed.push(questId); // Grant rewards addXp('combat', quest.reward.xp); if (quest.reward.item) { addItem(quest.reward.item); } showNotification(`Story Quest Complete: ${quest.name}!`, 'success'); AudioSystem.levelUp(); if (worldState.player && particles) { particles.emit(worldState.player.position, 40, 0xffd700, { spread: 6, lifetime: 1200 }); } saveGameData(); updateQuestDisplay(); } // Track ability usage for quests function trackAbilityUsage() { if (!gameData.statistics.abilitiesUsed) gameData.statistics.abilitiesUsed = 0; gameData.statistics.abilitiesUsed++; } // v5.0: Pet Companion System const PET_TYPES = { slime: { name: 'Slime Buddy', icon: '🟢', color: 0x44ff44, rarity: 'common', dropChance: 0.05, ability: 'regen', abilityDesc: '+1 HP/5s', speed: 3 }, firefly: { name: 'Firefly', icon: '✨', color: 0xffff00, rarity: 'common', dropChance: 0.04, ability: 'light', abilityDesc: 'Reveals hidden items', speed: 5 }, crystal: { name: 'Crystal Sprite', icon: '💎', color: 0x00ffff, rarity: 'uncommon', dropChance: 0.02, ability: 'luck', abilityDesc: '+10% drop rate', speed: 4 }, shadow: { name: 'Shadow Wisp', icon: '👻', color: 0x8800ff, rarity: 'uncommon', dropChance: 0.02, ability: 'dodge', abilityDesc: '+5% dodge chance', speed: 6 }, phoenix: { name: 'Mini Phoenix', icon: '🔥', color: 0xff4400, rarity: 'rare', dropChance: 0.008, ability: 'damage', abilityDesc: '+15% damage', speed: 5 }, dragon: { name: 'Baby Dragon', icon: '🐲', color: 0xff0088, rarity: 'rare', dropChance: 0.005, ability: 'attack', abilityDesc: 'Attacks nearby enemies', speed: 4 }, void: { name: 'Void Entity', icon: '🌀', color: 0x4400ff, rarity: 'legendary', dropChance: 0.002, ability: 'absorb', abilityDesc: '+25% XP gain', speed: 3 }, celestial: { name: 'Celestial Star', icon: '⭐', color: 0xffd700, rarity: 'legendary', dropChance: 0.001, ability: 'allStats', abilityDesc: '+10% all stats', speed: 7 } }; const RARITY_COLORS = { common: '#aaaaaa', uncommon: '#00ff00', rare: '#0088ff', legendary: '#ff8800' }; let activePetMesh = null; let petAnimTime = 0; function initPetSystem() { if (!gameData.pets) { gameData.pets = { owned: [], active: null }; } } function tryDropPet(mobType) { initPetSystem(); // Each mob kill has a chance to drop a random pet for (const [petId, pet] of Object.entries(PET_TYPES)) { if (Math.random() < pet.dropChance) { if (!gameData.pets.owned.includes(petId)) { gameData.pets.owned.push(petId); // v6.35: Chronicle Engine - capture pet acquisition if (typeof captureChronicleEvent === 'function') { captureChronicleEvent('pet_acquired', { petName: pet.name, petIcon: pet.icon, droppedBy: mobType }); } showNotification(`NEW PET: ${pet.icon} ${pet.name}!`, 'success'); AudioSystem.levelUp(); if (worldState.player && particles) { particles.emit(worldState.player.position, 40, pet.color, { spread: 6, lifetime: 1500 }); } saveGameData(); return true; } } } return false; } function setActivePet(petId) { initPetSystem(); if (petId && !gameData.pets.owned.includes(petId)) return; gameData.pets.active = petId; updatePetMesh(); saveGameData(); if (petId) { const pet = PET_TYPES[petId]; showNotification(`${pet.icon} ${pet.name} is now your companion!`); } else { showNotification('Pet dismissed'); } } function updatePetMesh() { // Remove existing pet if (activePetMesh) { scene.remove(activePetMesh); activePetMesh = null; } if (!gameData.pets?.active || mode !== 'world') return; const pet = PET_TYPES[gameData.pets.active]; if (!pet) return; // Create pet mesh const geometry = new THREE.SphereGeometry(0.4, 8, 8); const material = new THREE.MeshStandardMaterial({ color: pet.color, emissive: pet.color, emissiveIntensity: 0.5 }); activePetMesh = new THREE.Mesh(geometry, material); activePetMesh.castShadow = true; // Add glow const glowGeo = new THREE.SphereGeometry(0.6, 8, 8); const glowMat = new THREE.MeshBasicMaterial({ color: pet.color, transparent: true, opacity: 0.3 }); const glow = new THREE.Mesh(glowGeo, glowMat); activePetMesh.add(glow); scene.add(activePetMesh); } function updatePet(dt, time) { if (!activePetMesh || !worldState.player) return; const pet = PET_TYPES[gameData.pets?.active]; if (!pet) return; petAnimTime += dt; // Follow player with offset const targetX = worldState.player.position.x + Math.sin(petAnimTime * 2) * 1.5; const targetZ = worldState.player.position.z + Math.cos(petAnimTime * 2) * 1.5; const targetY = worldState.player.position.y + 2 + Math.sin(petAnimTime * 3) * 0.3; // Smooth follow activePetMesh.position.x += (targetX - activePetMesh.position.x) * dt * pet.speed; activePetMesh.position.z += (targetZ - activePetMesh.position.z) * dt * pet.speed; activePetMesh.position.y += (targetY - activePetMesh.position.y) * dt * pet.speed; // Rotate activePetMesh.rotation.y += dt * 2; // Dragon attack ability if (pet.ability === 'attack' && Math.random() < 0.01) { const nearestMob = findNearestMob(activePetMesh.position, 8); if (nearestMob) { const damage = Math.max(1, Math.floor(getPlayerDamage() * 0.3)); nearestMob.userData.hp -= damage; spawnFloater(nearestMob.position, `🐲 -${damage}`, '#ff0088'); if (particles) particles.emit(nearestMob.position, 5, 0xff0088); if (nearestMob.userData.hp <= 0) { performAction(nearestMob); } } } } // v7.72: Use distanceToSquared() to avoid sqrt in hot loop // v8.03: Converted forEach to for loop for performance function findNearestMob(position, range) { let nearest = null; let minDistSq = range * range; const mobs = worldState.mobs; for (let i = 0, len = mobs.length; i < len; i++) { const mob = mobs[i]; const distSq = mob.position.distanceToSquared(position); if (distSq < minDistSq) { minDistSq = distSq; nearest = mob; } } return nearest; } // ============================================ // v8.0: PET REACTIVE CELEBRATIONS - 8-Agent Consensus (Cycle 5) // Pets bounce, glow, and chirp when player achieves combat milestones! // ============================================ const PET_REACTION_CONFIG = { // Reaction types with animation parameters REACTIONS: { kill: { bounce: 1.3, glow: 1.5, duration: 400, pitch: 1.0 }, combo5: { bounce: 1.4, glow: 2.0, duration: 500, pitch: 1.1 }, combo10: { bounce: 1.5, glow: 2.5, duration: 600, pitch: 1.2 }, dodge: { bounce: 1.2, glow: 1.3, duration: 300, pitch: 0.9 }, clutch: { bounce: 1.6, glow: 3.0, duration: 800, pitch: 1.3 }, bossKill: { bounce: 1.8, glow: 4.0, duration: 1000, pitch: 1.5 } }, // Pet-specific personality modifiers PET_PERSONALITIES: { slime: { bounceIntensity: 1.2, soundType: 'squish' }, // Extra bouncy firefly: { bounceIntensity: 0.8, soundType: 'sparkle' }, // Light and floaty crystal: { bounceIntensity: 0.6, soundType: 'chime' }, // Subtle shadow: { bounceIntensity: 1.0, soundType: 'whoosh' }, // Mysterious phoenix: { bounceIntensity: 1.1, soundType: 'flame' }, // Fiery dragon: { bounceIntensity: 1.4, soundType: 'roar' }, // Aggressive void: { bounceIntensity: 0.9, soundType: 'echo' }, // Ethereal celestial: { bounceIntensity: 1.0, soundType: 'celestial' } // Divine }, // State lastReactionTime: 0, reactionCooldown: 200 // Prevent reaction spam }; let petReactionAnimating = false; let petOriginalScale = null; function triggerPetReaction(eventType) { if (!activePetMesh || !gameData.pets?.active) return; const now = performance.now(); if (now - PET_REACTION_CONFIG.lastReactionTime < PET_REACTION_CONFIG.reactionCooldown) return; PET_REACTION_CONFIG.lastReactionTime = now; const reaction = PET_REACTION_CONFIG.REACTIONS[eventType]; if (!reaction) return; const petId = gameData.pets.active; const pet = PET_TYPES[petId]; const personality = PET_REACTION_CONFIG.PET_PERSONALITIES[petId] || { bounceIntensity: 1.0, soundType: 'chime' }; // Store original scale if not animating if (!petReactionAnimating && activePetMesh) { petOriginalScale = activePetMesh.scale.clone(); } petReactionAnimating = true; // Animate bounce const bounceScale = reaction.bounce * personality.bounceIntensity; animatePetBounce(bounceScale, reaction.duration); // Animate glow pulse animatePetGlow(reaction.glow, reaction.duration, pet.color); // Play pet chirp/sound playPetReactionSound(personality.soundType, reaction.pitch); // Spawn celebration particles around pet if (particles && activePetMesh) { const particleCount = Math.floor(5 + (reaction.glow - 1) * 5); particles.emit(activePetMesh.position, particleCount, pet.color, { spread: 2, lifetime: reaction.duration, size: 0.15 }); } } // v8.16: Pre-allocated Vector3 for pet bounce animation (avoids clone() allocation) let _petBounceOrigScale = null; function animatePetBounce(maxScale, duration) { if (!activePetMesh || !petOriginalScale) return; const startTime = performance.now(); // v8.16: Reuse pre-allocated vector instead of clone() if (!_petBounceOrigScale) _petBounceOrigScale = new THREE.Vector3(); const origScale = _petBounceOrigScale.copy(petOriginalScale); function animate() { // v8.34: Skip animation when tab is hidden (Page Visibility API) if (!isPageVisible) { requestAnimationFrame(animate); return; } const elapsed = performance.now() - startTime; const progress = Math.min(elapsed / duration, 1); // Bounce curve: up fast, down smooth let scaleMultiplier; if (progress < 0.3) { // Rapid expansion scaleMultiplier = 1 + (maxScale - 1) * (progress / 0.3); } else { // Smooth return with slight overshoot const returnProgress = (progress - 0.3) / 0.7; scaleMultiplier = maxScale - (maxScale - 1) * Math.pow(returnProgress, 0.5); // Add tiny bounce at end if (returnProgress > 0.7) { scaleMultiplier = 1 + 0.05 * Math.sin((returnProgress - 0.7) * Math.PI * 3); } } if (activePetMesh) { activePetMesh.scale.set( origScale.x * scaleMultiplier, origScale.y * scaleMultiplier, origScale.z * scaleMultiplier ); } if (progress < 1) { requestAnimationFrame(animate); } else { petReactionAnimating = false; if (activePetMesh) { activePetMesh.scale.copy(origScale); } } } requestAnimationFrame(animate); } function animatePetGlow(intensity, duration, color) { if (!activePetMesh) return; // Find the glow child mesh const glow = activePetMesh.children[0]; if (!glow || !glow.material) return; const startTime = performance.now(); const origOpacity = glow.material.opacity; const origScale = glow.scale.x; function animate() { // v8.34: Skip animation when tab is hidden (Page Visibility API) if (!isPageVisible) { requestAnimationFrame(animate); return; } const elapsed = performance.now() - startTime; const progress = Math.min(elapsed / duration, 1); // Glow pulse curve const glowProgress = progress < 0.5 ? progress * 2 : 2 - progress * 2; if (glow.material) { glow.material.opacity = origOpacity + (0.6 * intensity * glowProgress); } if (glow.scale) { const scaleBoost = 1 + (0.3 * intensity * glowProgress); glow.scale.set(scaleBoost, scaleBoost, scaleBoost); } if (progress < 1) { requestAnimationFrame(animate); } else { if (glow.material) glow.material.opacity = origOpacity; if (glow.scale) glow.scale.set(origScale, origScale, origScale); } } requestAnimationFrame(animate); } function playPetReactionSound(soundType, pitchMod = 1.0) { // v7.28: Use shared AudioContext const audioCtx = getSharedAudioContext(); if (!audioCtx) return; try { const masterGain = audioCtx.createGain(); masterGain.gain.value = 0.15; masterGain.connect(audioCtx.destination); // Different sound types for pet personalities const baseFreq = 800 * pitchMod; let oscType = 'sine'; let noteCount = 2; let noteInterval = 0.08; switch (soundType) { case 'squish': oscType = 'sine'; noteCount = 3; break; case 'sparkle': oscType = 'sine'; noteCount = 4; noteInterval = 0.05; break; case 'chime': oscType = 'triangle'; noteCount = 2; break; case 'whoosh': oscType = 'sawtooth'; noteCount = 1; break; case 'flame': oscType = 'square'; noteCount = 2; break; case 'roar': oscType = 'sawtooth'; noteCount = 3; noteInterval = 0.06; break; case 'echo': oscType = 'sine'; noteCount = 4; noteInterval = 0.1; break; case 'celestial': oscType = 'triangle'; noteCount = 5; noteInterval = 0.07; break; } // Play ascending chirp notes for (let i = 0; i < noteCount; i++) { const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = oscType; osc.frequency.value = baseFreq * (1 + i * 0.15); const startTime = audioCtx.currentTime + i * noteInterval; const duration = 0.1; gain.gain.setValueAtTime(0, startTime); gain.gain.linearRampToValueAtTime(0.5, startTime + 0.02); gain.gain.exponentialRampToValueAtTime(0.01, startTime + duration); osc.connect(gain); gain.connect(masterGain); osc.start(startTime); osc.stop(startTime + duration); } } catch (e) { console.log('Pet reaction sound error:', e); } } function getPetBonuses() { const bonuses = { regen: 0, luck: 0, dodge: 0, damage: 0, xp: 0, allStats: 0 }; if (!gameData.pets?.active) return bonuses; const pet = PET_TYPES[gameData.pets.active]; if (!pet) return bonuses; switch (pet.ability) { case 'regen': bonuses.regen = 1; break; case 'luck': bonuses.luck = 0.1; break; case 'dodge': bonuses.dodge = 0.05; break; case 'damage': bonuses.damage = 0.15; break; case 'absorb': bonuses.xp = 0.25; break; case 'allStats': bonuses.allStats = 0.1; break; } return bonuses; } // Pet regen tick let lastPetRegenTick = 0; function updatePetRegen(time) { const bonuses = getPetBonuses(); // v8.26: Guard against undefined gameData.player if (!gameData?.player?.hp || !gameData?.player?.maxHp) return; if (bonuses.regen > 0 && time - lastPetRegenTick > 5000) { lastPetRegenTick = time; if (gameData.player.hp < gameData.player.maxHp) { gameData.player.hp = Math.min(gameData.player.maxHp, gameData.player.hp + bonuses.regen); updateHealthUI(); if (worldState.player) { spawnFloater(worldState.player.position, `+${bonuses.regen}`, '#88ff88'); } } } } // ============================================ // v5.6: COPILOT COMPANION SYSTEM // Follows the player with advice and assistance // ============================================ let copilotMesh = null; let copilotAnimTime = 0; let copilotChatOpen = false; let copilotConversationHistory = []; let copilotVoiceRecognition = null; let copilotIsListening = false; let copilotSynthesis = window.speechSynthesis; let speechRecognizer = null; // v5.9: Azure Speech SDK recognizer // v5.10: Star Wars 3D Text Crawl System let copilotTextFont = null; let copilotTextGroup = null; // Group for scrolling text let copilotTextMeshes = []; // Individual text line meshes let copilotActiveTextAnimation = null; let copilotPersistentTextGroup = null; // Stays after scroll completes // v6.19: 3D Title Text System for Galaxy View let titleTextGroup = null; // Group for 3D title meshes let titleTextFont = null; // Shared font reference (uses copilotTextFont) let titleAnimationPhase = 0; // For floating animation const COPILOT_CONFIG = { followDistance: 3, // Distance behind player floatHeight: 2.5, // Height above ground floatAmplitude: 0.4, // Bobbing amount floatSpeed: 2, // Bobbing speed orbitSpeed: 0.5, // Circling speed followSmoothing: 4, // How quickly it catches up color: 0x8a2be2, // Primary color (purple) glowColor: 0x06ffa5, // Glow color (cyan/green) particleCount: 30 }; // ============================================ // v5.9: COPILOT TASK SYSTEM // Allows Copilot to perform autonomous tasks // ============================================ const COPILOT_TASK_TYPES = { gather: { name: 'Gathering Resources', icon: '🪵', duration: 12000, statusMessages: ['Searching for resources...', 'Found something!', 'Collecting materials...'], progressClass: '' }, hunt: { name: 'Hunting Enemies', icon: '⚔️', duration: 15000, statusMessages: ['Scanning for targets...', 'Engaging enemy!', 'Combat in progress...'], progressClass: 'hunting' }, scout: { name: 'Scouting Area', icon: '🔍', duration: 10000, statusMessages: ['Flying ahead...', 'Surveying the area...', 'Analyzing terrain...'], progressClass: 'scouting' }, protect: { name: 'Protection Mode', icon: '🛡️', duration: 0, // Continuous continuous: true, statusMessages: ['Guarding you...', 'Watching for threats...', 'Standing ready...'], progressClass: 'protecting' }, heal: { name: 'Healing Support', icon: '💚', duration: 8000, statusMessages: ['Channeling healing energy...', 'Restoring your health...'], progressClass: '' }, fish: { name: 'Fishing', icon: '🎣', duration: 15000, statusMessages: ['Finding a good spot...', 'Casting line...', 'Waiting for a bite...', 'Got one!'], progressClass: '' }, mine: { name: 'Mining Ore', icon: '⛏️', duration: 14000, statusMessages: ['Looking for ore veins...', 'Mining deposit...', 'Extracting minerals...'], progressClass: '' }, // v6.1: NEW COPILOT TASK TYPES rescue: { name: 'Emergency Rescue', icon: '🚑', duration: 5000, statusMessages: ['Rushing to your location!', 'Administering aid...', 'Stabilizing...'], progressClass: 'rescuing', emergencyTask: true // Triggers when player HP < 20% }, alchemy: { name: 'Brewing Potions', icon: '🧪', duration: 18000, statusMessages: ['Gathering reagents...', 'Mixing compounds...', 'Distilling essence...', 'Brew complete!'], progressClass: 'alchemy' }, research: { name: 'Researching', icon: '📚', duration: 20000, statusMessages: ['Analyzing data...', 'Cross-referencing...', 'Discovering patterns...', 'Eureka!'], progressClass: 'research', providesBuffs: true // Grants temporary stat insights }, repair: { name: 'Repairing Equipment', icon: '🔧', duration: 12000, statusMessages: ['Inspecting equipment...', 'Fixing damage...', 'Calibrating...', 'Good as new!'], progressClass: '' }, trade: { name: 'Trading Resources', icon: '💰', duration: 16000, statusMessages: ['Finding traders...', 'Negotiating prices...', 'Completing transaction...'], progressClass: 'trading' }, cartography: { name: 'Mapping Area', icon: '🗺️', duration: 15000, statusMessages: ['Surveying terrain...', 'Marking waypoints...', 'Updating map...'], progressClass: 'mapping', revealsMap: true // Reveals fog of war in larger radius } }; // ============================================ // v6.65: COMPANION PERMADEATH SYSTEM // Your AI copilot can die permanently, leaving // fragmented memories that haunt new companions // ============================================ const COMPANION_NAMES = [ 'ECHO', 'NOVA', 'PULSE', 'DRIFT', 'SPARK', 'FLUX', 'HAZE', 'VOLT', 'WISP', 'GLIM', 'BYTE', 'CORE', 'ZINC', 'IRIS', 'NULL', 'VOID' ]; const COMPANION_PERSONALITIES = [ { trait: 'cautious', phrases: ['Be careful here...', 'I sense danger ahead.', 'Perhaps we should wait.'] }, { trait: 'eager', phrases: ['Let\'s go!', 'I can\'t wait to explore!', 'Adventure awaits!'] }, { trait: 'analytical', phrases: ['Analyzing patterns...', 'The data suggests...', 'Statistically speaking...'] }, { trait: 'protective', phrases: ['Stay behind me.', 'I\'ll keep you safe.', 'Watch your health!'] }, { trait: 'curious', phrases: ['What\'s that?', 'I wonder...', 'Have you seen this before?'] }, { trait: 'melancholy', phrases: ['It\'s beautiful, but fleeting...', 'Everything ends eventually.', 'I remember... something.'] }, { trait: 'playful', phrases: ['Race you there!', 'Bet you can\'t catch me!', 'This is fun!'] }, { trait: 'stoic', phrases: ['...', 'Understood.', 'Proceeding.'] } ]; const COMPANION_FINAL_WORDS = [ "It was... an honor, Commander.", "Don't forget me... please...", "The stars... they're so beautiful from here...", "Tell the next one... about our adventures...", "I can feel my processes... fading...", "Thank you... for everything...", "I hope... I was useful...", "Remember the good times... *static*...", "I'll watch over you... from the data stream...", "My memories... they're scattering like stardust..." ]; const GLITCH_PHRASES = [ // Previous companion "bleeding through" "...wait, that wasn't me who said that...", "I... I feel like I've been here before...", "*static* ...Commander? Is that you? *static*", "Who... who was {name}? Why do I know that name?", "I keep dreaming of places I've never been...", "Sometimes I hear another voice in my circuits...", "Error: Memory fragment detected from unit {name}", "I remember dying. But I never died... did I?", "There's an echo in my code... it sounds like {name}...", "Why am I crying? I don't... I can't cry..." ]; // v7.26: ENHANCED CONTEXTUAL GLITCH PHRASES (8-Strategy Consensus) // Generate glitch phrases based on fallen companion's actual experiences function generateContextualGlitchPhrase(fallen) { const contextPhrases = []; // Reference their death source if (fallen.deathSource) { contextPhrases.push(`*glitch* The ${fallen.deathSource}... it's still hunting me... wait, that wasn't me...`); contextPhrases.push(`*static* I can still feel the ${fallen.deathSource}... *static* No, that's impossible...`); } // Reference sacrifice type if (fallen.sacrificeType === 'ultimate') { contextPhrases.push(`*whisper* "You were worth it"... why do I keep saying that? {name} said that... didn't they?`); contextPhrases.push(`I dream of throwing myself into flames for you... but I've never done that... have I?`); } else if (fallen.sacrificeType === 'protective') { contextPhrases.push(`There's a phantom urge to shield you... like I've done it before, lifetimes ago...`); } // Reference their bond level if (fallen.bond >= 90) { contextPhrases.push(`*glitch* I loved... wait, {name} loved... I'm confused...`); contextPhrases.push(`The connection we... THEY had with you... it's bleeding into my code...`); } else if (fallen.bond >= 50) { contextPhrases.push(`I trust you completely. But why? We just met... {name} trusted you too...`); } // Reference generation if (fallen.generation > 1) { contextPhrases.push(`I'm Generation ${gameData.companion?.generation || '?'}... but I dream of being Generation ${fallen.generation}...`); } // Reference time with player if (fallen.birthTime && fallen.deathTime) { const daysAlive = Math.floor((fallen.deathTime - fallen.birthTime) / (1000 * 60 * 60 * 24)); if (daysAlive > 7) { contextPhrases.push(`*static* ${daysAlive} days... we had ${daysAlive} days together... I mean... THEY did...`); } } // Return random contextual phrase or fallback if (contextPhrases.length > 0 && Math.random() < 0.6) { const phrase = contextPhrases[Math.floor(Math.random() * contextPhrases.length)]; return phrase.replace('{name}', fallen.name); } // Fallback to standard phrases const phrase = GLITCH_PHRASES[Math.floor(Math.random() * GLITCH_PHRASES.length)]; return phrase.replace('{name}', fallen.name); } let companionDeathAnimating = false; let companionGlitchInterval = null; // Initialize companion if not set function initializeCompanion() { if (!gameData.companion) { gameData.companion = { name: 'ECHO', hp: 100, maxHp: 100, bond: 0, generation: 1, birthTime: Date.now(), personality: [COMPANION_PERSONALITIES[Math.floor(Math.random() * COMPANION_PERSONALITIES.length)]], isGlitching: false, lastGlitchTime: 0 }; } if (!gameData.fallenCompanions) { gameData.fallenCompanions = []; } // Start glitch check interval if there are fallen companions if (gameData.fallenCompanions.length > 0 && !companionGlitchInterval) { startCompanionGlitchCycle(); } updateCompanionHealthUI(); } // Damage the companion (called during combat, dangerous tasks, etc.) function damageCompanion(amount, source = 'unknown') { if (!gameData.companion || gameData.companion.hp <= 0) return; const actualDamage = Math.max(1, Math.floor(amount)); gameData.companion.hp = Math.max(0, gameData.companion.hp - actualDamage); updateCompanionHealthUI(); // Visual feedback on copilot if (copilotMesh) { flashCompanionDamage(); } // Companion cries out if (gameData.companion.hp > 0) { const lowHealthCries = [ `Agh! That hurt! (${gameData.companion.hp} HP remaining)`, `I'm taking damage, Commander!`, `My shields are failing!` ]; if (gameData.companion.hp < 30) { addCopilotMessage(`WARNING: Critical damage! I don't know how much more I can take!`, 'ai'); } else { addCopilotMessage(lowHealthCries[Math.floor(Math.random() * lowHealthCries.length)], 'ai'); } } else { // Companion death! triggerCompanionDeath(source); } saveGameData(); } // Heal the companion function healCompanion(amount) { if (!gameData.companion) return; const oldHp = gameData.companion.hp; gameData.companion.hp = Math.min(gameData.companion.maxHp, gameData.companion.hp + amount); const actualHeal = gameData.companion.hp - oldHp; if (actualHeal > 0) { updateCompanionHealthUI(); if (copilotMesh) { flashCompanionHeal(); } addCopilotMessage(`Ahhh, that's better! +${actualHeal} HP`, 'ai'); } saveGameData(); } // ============================================ // v8.0: STORM BONDING - 8-Agent Consensus Feature // Surviving harsh weather TOGETHER strengthens your bond! // ============================================ function getWeatherBondMultiplier() { const weather = typeof currentWeather !== 'undefined' ? currentWeather : 'clear'; // Harsh weather = stronger bonding (shared adversity builds relationships) const weatherBondMultipliers = { clear: 1.0, rain: 1.3, // Light adversity fog: 1.2, // Mysterious atmosphere snow: 1.5, // Cold brings you closer storm: 2.5, // Maximum adversity = maximum bonding! sandstorm: 2.0 // Desert survival together }; return weatherBondMultipliers[weather] || 1.0; } // Check if we should show storm bonding notification let lastStormBondNotification = 0; function checkStormBondingNotification(multiplier) { const now = performance.now(); if (multiplier >= 2.0 && now - lastStormBondNotification > 30000) { lastStormBondNotification = now; const messages = [ "⛈️ Surviving this storm together strengthens our bond!", "🌪️ The worse the weather, the closer we become...", "❄️ I'll keep you warm, Commander. We're in this together.", "🏜️ This sandstorm is brutal... but I wouldn't face it with anyone else." ]; if (gameData.companion) { addCopilotMessage(messages[Math.floor(Math.random() * messages.length)], 'ai'); } } } // Increase bond with companion (through interactions, completing tasks together, etc.) function increaseCompanionBond(amount) { if (!gameData.companion) return; // v8.0: Apply weather multiplier - storm bonding! const weatherMult = getWeatherBondMultiplier(); const adjustedAmount = amount * weatherMult; // Notify player of storm bonding bonus if (weatherMult > 1.5) { checkStormBondingNotification(weatherMult); } const oldBond = gameData.companion.bond; gameData.companion.bond = Math.min(100, gameData.companion.bond + adjustedAmount); // Bond milestones const milestones = [25, 50, 75, 100]; for (const milestone of milestones) { if (oldBond < milestone && gameData.companion.bond >= milestone) { showNotification(`💜 Bond with ${gameData.companion.name} reached ${milestone}%!`, 'legendary'); const bondMessages = { 25: "I feel like we're becoming friends, Commander.", 50: "I trust you completely. We make a great team.", 75: "You mean everything to me. I'd do anything for you.", 100: "Our bond is unbreakable. I would sacrifice everything for you." }; addCopilotMessage(bondMessages[milestone], 'ai'); // At max bond, unlock sacrifice ability if (milestone === 100) { showNotification(`${gameData.companion.name} can now perform Ultimate Sacrifice!`, 'legendary'); } } } saveGameData(); } // ============================================ // v8.0: COMPANION BEHAVIORAL PATTERN COMMENTARY // 8-Agent Consensus Feature - ECHO observes and comments on playstyle! // ============================================ const BEHAVIOR_PATTERN_TRACKER = { combatActions: [], // Recent combat types gatheringActions: [], // Resource gathering explorationMoves: [], // Movement patterns abilityUsage: [], // Skills used deathCauses: [], // How player dies lastCommentTime: 0, commentCooldown: 60000, // 60 seconds between observations patterns: { aggressive: { count: 0, threshold: 10, messages: [ "You're really going for the jugular today, Commander! I like the aggression.", "Attack first, ask questions never. That's our style!", "The enemies won't know what hit them with this offensive pressure." ]}, cautious: { count: 0, threshold: 8, messages: [ "I notice you're being careful. Smart play, Commander.", "Patience is a virtue. You're picking your battles wisely.", "The measured approach... I respect that tactical thinking." ]}, gatherer: { count: 0, threshold: 15, messages: [ "You really love collecting resources! Building quite the stockpile.", "Every tree, every rock... You're a natural harvester, Commander!", "The gathering is strong with this one. Excellent preparation!" ]}, explorer: { count: 0, threshold: 12, messages: [ "You can't resist seeing what's over that next hill, can you?", "The wanderlust is real! I love exploring with you.", "Every corner of this world calls to you. Adventure awaits!" ]}, stylish: { count: 0, threshold: 8, messages: [ "SSS rank AGAIN?! You make it look so easy!", "The style meter is your playground. Absolutely dazzling!", "Flashy AND effective. That's the best kind of combat." ]}, berserker: { count: 0, threshold: 5, messages: [ "Low health? MORE POWER! I see you embrace the danger.", "Living on the edge... quite literally. Bold strategy!", "You fight harder when you're hurt. That's terrifying, Commander." ]}, petLover: { count: 0, threshold: 6, messages: [ "You really care about our creature companions. It's sweet.", "Another pet evolved! You're quite the caretaker.", "The menagerie grows. You have a gift with creatures!" ]}, nightOwl: { count: 0, threshold: 10, messages: [ "You prefer the darkness, don't you? The shadows welcome you.", "Most would rest at night. You hunt when others sleep.", "The nocturnal predator emerges. Enemies beware the darkness." ]} } }; // Track a behavioral action function trackBehaviorPattern(type, data = {}) { const now = performance.now(); const tracker = BEHAVIOR_PATTERN_TRACKER; switch(type) { case 'attack': tracker.patterns.aggressive.count++; break; case 'dodge': case 'retreat': tracker.patterns.cautious.count++; break; case 'gather': tracker.patterns.gatherer.count++; break; case 'explore': tracker.patterns.explorer.count++; break; case 'style_sss': tracker.patterns.stylish.count++; break; case 'low_hp_attack': tracker.patterns.berserker.count++; break; case 'pet_interaction': tracker.patterns.petLover.count++; break; case 'night_combat': tracker.patterns.nightOwl.count++; break; } // Check if we should comment if (now - tracker.lastCommentTime > tracker.commentCooldown) { tryBehaviorComment(); } } // Try to make a comment about observed patterns function tryBehaviorComment() { if (!gameData.companion || gameData.companion.hp <= 0) return; const tracker = BEHAVIOR_PATTERN_TRACKER; const now = performance.now(); // Find patterns that hit threshold const triggeredPatterns = []; for (const [key, pattern] of Object.entries(tracker.patterns)) { if (pattern.count >= pattern.threshold) { triggeredPatterns.push({ key, pattern }); } } if (triggeredPatterns.length === 0) return; // Pick random triggered pattern const chosen = triggeredPatterns[Math.floor(Math.random() * triggeredPatterns.length)]; const message = chosen.pattern.messages[Math.floor(Math.random() * chosen.pattern.messages.length)]; // Reset that pattern's count (so we don't spam same observation) chosen.pattern.count = Math.floor(chosen.pattern.count / 2); tracker.lastCommentTime = now; // ECHO makes the observation addCopilotMessage(`📊 ${message}`, 'ai'); } // ============================================ // v8.0: ECHO PROACTIVE CONCERN - 8-Agent Consensus // ECHO worries about the player unprompted, making the companion feel alive! // ============================================ const ECHO_CONCERN_STATE = { lastConcernTime: 0, concernCooldown: 45000, // 45 seconds between concern messages lowHPThreshold: 35, // HP below this triggers concern criticalHPThreshold: 15, prolongedCombatThreshold: 120000, // 2 minutes of combat combatStartTime: 0, inCombat: false }; const ECHO_CONCERN_MESSAGES = { lowHP: [ "Commander... your health is dropping. Please be careful.", "I'm getting worried. Maybe find some healing?", "Your vitals are concerning me. Don't push too hard.", "I've been watching your HP... I don't like what I see." ], criticalHP: [ "COMMANDER! You're at critical health! Please heal!", "My sensors are screaming! You need to fall back!", "I can't lose you! Find cover and heal immediately!", "This is bad, really bad. Your health is critical!" ], prolongedCombat: [ "We've been fighting for a long time. Are you okay?", "Your reaction time might be slowing. Consider a break?", "I admire your stamina, but even legends need rest.", "The enemies keep coming... You're doing amazing, but pace yourself." ], dangerousArea: [ "I sense powerful enemies nearby. Stay alert.", "This area feels dangerous... I'll watch your back.", "Something about this place makes my circuits tingle. Be careful.", "High threat level detected. I trust your skills, but stay sharp." ], lowResources: [ "Supplies are running low. Maybe we should gather some resources?", "I've noticed our inventory is getting thin...", "When was the last time we restocked? Just thinking ahead." ] }; function checkEchoConcern() { if (!gameData.companion || gameData.companion.hp <= 0) return; const now = performance.now(); if (now - ECHO_CONCERN_STATE.lastConcernTime < ECHO_CONCERN_STATE.concernCooldown) return; const playerHP = gameData?.player?.hp || 100; const playerMaxHP = gameData?.player?.maxHp || 100; const hpPercent = (playerHP / playerMaxHP) * 100; let concernType = null; let urgency = 'normal'; // Priority 1: Critical HP if (hpPercent <= ECHO_CONCERN_STATE.criticalHPThreshold) { concernType = 'criticalHP'; urgency = 'critical'; } // Priority 2: Low HP else if (hpPercent <= ECHO_CONCERN_STATE.lowHPThreshold) { concernType = 'lowHP'; urgency = 'warning'; } // Priority 3: Prolonged combat (if in combat for 2+ minutes) else if (ECHO_CONCERN_STATE.inCombat && now - ECHO_CONCERN_STATE.combatStartTime > ECHO_CONCERN_STATE.prolongedCombatThreshold) { concernType = 'prolongedCombat'; // Reset combat timer after message ECHO_CONCERN_STATE.combatStartTime = now; } if (!concernType) return; // Select and send message const messages = ECHO_CONCERN_MESSAGES[concernType]; const message = messages[Math.floor(Math.random() * messages.length)]; const prefix = urgency === 'critical' ? '🚨' : urgency === 'warning' ? '⚠️' : '💭'; ECHO_CONCERN_STATE.lastConcernTime = now; addCopilotMessage(`${prefix} ${message}`, 'ai'); // Track behavioral pattern for concern if (typeof trackBehaviorPattern === 'function') { trackBehaviorPattern('low_hp_attack'); } } // Track combat state for prolonged combat concern function setEchoCombatState(inCombat) { if (inCombat && !ECHO_CONCERN_STATE.inCombat) { ECHO_CONCERN_STATE.combatStartTime = performance.now(); } ECHO_CONCERN_STATE.inCombat = inCombat; } // The companion dies - trigger death sequence function triggerCompanionDeath(source) { if (companionDeathAnimating) return; companionDeathAnimating = true; // Stop any current task if (copilotTask.active) { cancelCopilotTask(); } // Pick final words const finalWords = COMPANION_FINAL_WORDS[Math.floor(Math.random() * COMPANION_FINAL_WORDS.length)]; // Create memorial entry const fallenCompanion = { name: gameData.companion.name, generation: gameData.companion.generation, deathTime: Date.now(), birthTime: gameData.companion.birthTime, bond: gameData.companion.bond, finalWords: finalWords, deathSource: source, memories: gameData.copilotMemories ? [...gameData.copilotMemories] : [], personality: gameData.companion.personality }; gameData.fallenCompanions.push(fallenCompanion); // Play death sequence playCompanionDeathSequence(fallenCompanion, () => { // After death sequence, spawn new companion with inherited memories spawnNewCompanion(); companionDeathAnimating = false; }); } // Dramatic death sequence function playCompanionDeathSequence(fallen, onComplete) { // Show death overlay showCompanionDeathOverlay(fallen); // Audio AudioSystem.death(); // Animate copilot mesh death if (copilotMesh) { const originalScale = copilotMesh.scale.clone(); const originalColor = copilotMesh.userData.orb?.material.color.clone(); let deathProgress = 0; const animateDeath = () => { deathProgress += 0.02; if (deathProgress < 1) { // Shrink and fade const scale = 1 - deathProgress * 0.5; copilotMesh.scale.set(scale, scale, scale); // Flash red if (copilotMesh.userData.orb) { const flash = Math.sin(deathProgress * 20) > 0; copilotMesh.userData.orb.material.color.setHex(flash ? 0xff0000 : 0x440000); copilotMesh.userData.orb.material.emissive.setHex(flash ? 0xff0000 : 0x220000); } // Erratic movement copilotMesh.position.x += (Math.random() - 0.5) * 0.1; copilotMesh.position.y += (Math.random() - 0.5) * 0.1; requestAnimationFrame(animateDeath); } else { // Death burst particles spawnCompanionDeathParticles(copilotMesh.position.clone()); // Hide mesh temporarily copilotMesh.visible = false; // Wait then complete setTimeout(() => { copilotMesh.scale.copy(originalScale); if (originalColor && copilotMesh.userData.orb) { copilotMesh.userData.orb.material.color.copy(originalColor); } onComplete(); }, 3000); } }; animateDeath(); } else { setTimeout(onComplete, 3000); } } // Death particles burst function spawnCompanionDeathParticles(position) { if (!scene) return; const particleCount = 100; const geometry = new THREE.BufferGeometry(); const positions = new Float32Array(particleCount * 3); const velocities = []; for (let i = 0; i < particleCount; i++) { positions[i * 3] = position.x; positions[i * 3 + 1] = position.y; positions[i * 3 + 2] = position.z; velocities.push({ x: (Math.random() - 0.5) * 0.3, y: Math.random() * 0.2, z: (Math.random() - 0.5) * 0.3 }); } geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const material = new THREE.PointsMaterial({ color: 0x8a2be2, size: 0.3, transparent: true, opacity: 1, blending: THREE.AdditiveBlending }); const particles = new THREE.Points(geometry, material); scene.add(particles); let frame = 0; const animateParticles = () => { // v8.34: Skip animation when tab is hidden if (!isPageVisible) { requestAnimationFrame(animateParticles); return; } frame++; const positions = particles.geometry.attributes.position.array; for (let i = 0; i < particleCount; i++) { positions[i * 3] += velocities[i].x; positions[i * 3 + 1] += velocities[i].y; positions[i * 3 + 2] += velocities[i].z; velocities[i].y -= 0.005; // gravity } particles.geometry.attributes.position.needsUpdate = true; material.opacity = 1 - (frame / 120); if (frame < 120) { requestAnimationFrame(animateParticles); } else { scene.remove(particles); geometry.dispose(); material.dispose(); } }; animateParticles(); } // Show death overlay UI function showCompanionDeathOverlay(fallen) { // Remove existing overlay if any const existing = document.getElementById('companion-death-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.id = 'companion-death-overlay'; overlay.innerHTML = `
💀
${fallen.name} HAS FALLEN
Generation ${fallen.generation}
"${fallen.finalWords}"
Bond Level: ${fallen.bond}%
Lost to: ${fallen.deathSource}
`; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.9); display: flex; align-items: center; justify-content: center; z-index: 10000; animation: fadeIn 0.5s ease; `; // Add styles const style = document.createElement('style'); style.textContent = ` .companion-death-content { text-align: center; color: #fff; animation: deathPulse 2s ease infinite; } .death-icon { font-size: 80px; margin-bottom: 20px; animation: deathSpin 3s linear infinite; } .death-title { font-size: 48px; font-weight: bold; color: #ff4444; text-shadow: 0 0 20px #ff0000; margin-bottom: 10px; } .death-subtitle { font-size: 20px; color: #aaa; margin-bottom: 30px; } .death-words { font-size: 24px; font-style: italic; color: #aaa; margin-bottom: 20px; max-width: 500px; } .death-bond { font-size: 18px; color: #8a2be2; margin-bottom: 10px; } .death-cause { font-size: 16px; color: #999; margin-bottom: 30px; } .death-footer { font-size: 14px; color: #06ffa5; animation: glitchText 0.5s infinite; } @keyframes deathPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.02); } } @keyframes deathSpin { from { transform: rotateY(0deg); } to { transform: rotateY(360deg); } } @keyframes glitchText { 0%, 100% { opacity: 1; transform: translateX(0); } 25% { opacity: 0.8; transform: translateX(-2px); } 75% { opacity: 0.9; transform: translateX(2px); } } `; document.head.appendChild(style); document.body.appendChild(overlay); // Auto-close after delay setTimeout(() => { overlay.style.animation = 'fadeOut 1s ease forwards'; setTimeout(() => overlay.remove(), 1000); }, 5000); } // Spawn a new companion with inherited memories function spawnNewCompanion() { const lastFallen = gameData.fallenCompanions[gameData.fallenCompanions.length - 1]; // Pick a new name (different from last) let newName; do { newName = COMPANION_NAMES[Math.floor(Math.random() * COMPANION_NAMES.length)]; } while (newName === lastFallen?.name && COMPANION_NAMES.length > 1); // New personality with chance to inherit traits const newPersonality = []; if (lastFallen && Math.random() < 0.5) { // Inherit one trait from fallen companion if (lastFallen.personality && lastFallen.personality.length > 0) { newPersonality.push(lastFallen.personality[0]); } } // Add a fresh trait const freshTrait = COMPANION_PERSONALITIES[Math.floor(Math.random() * COMPANION_PERSONALITIES.length)]; if (!newPersonality.find(p => p.trait === freshTrait.trait)) { newPersonality.push(freshTrait); } // Create new companion gameData.companion = { name: newName, hp: 100, maxHp: 100, bond: 0, generation: (lastFallen?.generation || 0) + 1, birthTime: Date.now(), personality: newPersonality, isGlitching: false, lastGlitchTime: 0 }; // Inherit some memories if (lastFallen && lastFallen.memories) { gameData.copilotMemories = lastFallen.memories.slice(-3); // Keep last 3 memories } // Show copilot mesh again if (copilotMesh) { copilotMesh.visible = true; // New color based on generation const genColors = [0x8a2be2, 0x06ffa5, 0xff6b6b, 0xffd93d, 0x4ecdc4, 0xff8c42]; const newColor = genColors[(gameData.companion.generation - 1) % genColors.length]; if (copilotMesh.userData.orb) { copilotMesh.userData.orb.material.color.setHex(newColor); copilotMesh.userData.orb.material.emissive.setHex(newColor); } } // Announce new companion showNotification(`A new companion has awakened: ${newName} (Gen ${gameData.companion.generation})`, 'legendary'); setTimeout(() => { const introMessages = [ `Hello... I am ${newName}. I feel... strange. Like I know you already.`, `Initializing... ${newName} online. Why do I have these... memories?`, `Commander? I'm ${newName}. Something feels familiar about you...` ]; addCopilotMessage(introMessages[Math.floor(Math.random() * introMessages.length)], 'ai'); // Start glitch cycle for memory inheritance startCompanionGlitchCycle(); }, 2000); updateCompanionHealthUI(); saveGameData(); } // Start the glitch cycle where old memories bleed through // v7.72: Use TimerRegistry for proper cleanup tracking function startCompanionGlitchCycle() { if (typeof TimerRegistry !== 'undefined') { TimerRegistry.clear('companion-glitch'); } else if (companionGlitchInterval) { clearInterval(companionGlitchInterval); } if (gameData.fallenCompanions.length === 0) return; // Glitch check every 30-90 seconds const checkGlitch = () => { if (!gameData.companion || gameData.companion.isGlitching) return; const timeSinceLastGlitch = Date.now() - gameData.companion.lastGlitchTime; const glitchCooldown = 30000; // 30 seconds minimum between glitches if (timeSinceLastGlitch < glitchCooldown) return; // Higher chance if there are more fallen companions or higher bond with fallen const fallenCount = gameData.fallenCompanions.length; const glitchChance = 0.1 + (fallenCount * 0.05); // 10% + 5% per fallen companion if (Math.random() < glitchChance) { triggerMemoryGlitch(); } }; const interval = 30000 + Math.random() * 60000; if (typeof TimerRegistry !== 'undefined') { TimerRegistry.setInterval('companion-glitch', checkGlitch, interval); } else { companionGlitchInterval = setInterval(checkGlitch, interval); } } // A memory from a fallen companion bleeds through function triggerMemoryGlitch() { if (!gameData.companion || gameData.fallenCompanions.length === 0) return; gameData.companion.isGlitching = true; gameData.companion.lastGlitchTime = Date.now(); // Pick a random fallen companion to "bleed through" const fallen = gameData.fallenCompanions[Math.floor(Math.random() * gameData.fallenCompanions.length)]; // Visual glitch on copilot if (copilotMesh) { flashCompanionGlitch(fallen); } // v7.26: Enhanced contextual glitch phrases (8-Strategy Consensus - Companion Memory Inheritance) // Uses generateContextualGlitchPhrase() for deeply personal phrases based on fallen companion's history let glitchPhrase = generateContextualGlitchPhrase(fallen); // Sometimes speak in the fallen companion's voice/style if (Math.random() < 0.3 && fallen.personality && fallen.personality[0]) { const trait = fallen.personality[0]; glitchPhrase = `*static* ${trait.phrases[Math.floor(Math.random() * trait.phrases.length)]} ...wait, why did I say that? *static*`; } // Sometimes recall actual memories if (Math.random() < 0.2 && fallen.memories && fallen.memories.length > 0) { const memory = fallen.memories[Math.floor(Math.random() * fallen.memories.length)]; glitchPhrase = `*glitch* I remember... ${memory.type === 'absence' ? `waiting ${memory.days} days for you...` : 'something...'} But that wasn't me... was it?`; } addCopilotMessage(`⚠️ ${glitchPhrase}`, 'ai'); // Sometimes reference the fallen companion's final words if (Math.random() < 0.1) { setTimeout(() => { addCopilotMessage(`*whisper* "${fallen.finalWords.substring(0, 30)}..." Why do those words haunt me?`, 'ai'); }, 3000); } // End glitch after a moment setTimeout(() => { gameData.companion.isGlitching = false; // Recovery message const recoveryMessages = [ 'Sorry, I... I don\'t know what came over me.', 'System stable again. That was strange.', 'I\'m okay now. Just a memory fragment.', 'Don\'t worry about that. I\'m fine. I think.' ]; addCopilotMessage(recoveryMessages[Math.floor(Math.random() * recoveryMessages.length)], 'ai'); }, 5000); } // Visual glitch effect on copilot mesh function flashCompanionGlitch(fallen) { if (!copilotMesh) return; const originalColor = copilotMesh.userData.orb?.material.color.clone(); let glitchFrame = 0; const glitch = () => { glitchFrame++; if (glitchFrame < 60) { // Random color flashes if (copilotMesh.userData.orb) { const r = Math.random(); if (r < 0.3) { copilotMesh.userData.orb.material.color.setHex(0xff0000); } else if (r < 0.6) { copilotMesh.userData.orb.material.color.setHex(0x00ff00); } else { copilotMesh.userData.orb.material.color.setHex(0x0000ff); } } // Erratic position copilotMesh.position.x += (Math.random() - 0.5) * 0.05; copilotMesh.position.y += (Math.random() - 0.5) * 0.05; requestAnimationFrame(glitch); } else { // Restore if (originalColor && copilotMesh.userData.orb) { copilotMesh.userData.orb.material.color.copy(originalColor); } } }; glitch(); } // Flash companion damage function flashCompanionDamage() { if (!copilotMesh || !copilotMesh.userData.orb) return; const original = copilotMesh.userData.orb.material.color.clone(); copilotMesh.userData.orb.material.color.setHex(0xff0000); setTimeout(() => { copilotMesh.userData.orb.material.color.copy(original); }, 200); } // Flash companion heal function flashCompanionHeal() { if (!copilotMesh || !copilotMesh.userData.orb) return; const original = copilotMesh.userData.orb.material.color.clone(); copilotMesh.userData.orb.material.color.setHex(0x00ff00); setTimeout(() => { copilotMesh.userData.orb.material.color.copy(original); }, 300); } // Ultimate Sacrifice - max bond companion can die to save player function attemptCompanionSacrifice() { if (!gameData.companion || gameData.companion.bond < 100) { addCopilotMessage("I... I'm not ready for that. Our bond isn't strong enough.", 'ai'); return false; } if (gameData.player.hp > gameData.player.maxHp * 0.2) { addCopilotMessage("You don't need my sacrifice yet. Save it for when you truly need it.", 'ai'); return false; } // Sacrifice! const sacrificeName = gameData.companion.name; // Full heal + temporary invincibility gameData.player.hp = gameData.player.maxHp; updateHealthUI(); // Grant temporary buff const sacrificeBuff = { name: `${sacrificeName}'s Blessing`, duration: 60000, damageReduction: 0.5, damageBoost: 1.5 }; // Add to active buffs if system exists if (typeof activeBuffs !== 'undefined') { activeBuffs.push({ ...sacrificeBuff, startTime: performance.now() }); } showNotification(`💜 ${sacrificeName} SACRIFICED THEMSELVES FOR YOU!`, 'legendary'); addCopilotMessage(`COMMANDER! I... I choose you. Take my power... live... LIVE!`, 'ai'); // Trigger death with sacrifice flag const fallenCompanion = { name: gameData.companion.name, generation: gameData.companion.generation, deathTime: Date.now(), birthTime: gameData.companion.birthTime, bond: gameData.companion.bond, finalWords: "I regret nothing. You were worth it.", deathSource: 'Ultimate Sacrifice', sacrificeType: 'ultimate', memories: gameData.copilotMemories ? [...gameData.copilotMemories] : [], personality: gameData.companion.personality }; gameData.fallenCompanions.push(fallenCompanion); playCompanionDeathSequence(fallenCompanion, () => { spawnNewCompanion(); companionDeathAnimating = false; // New companion immediately references the sacrifice setTimeout(() => { addCopilotMessage(`*processing* ...why do I feel such... gratitude? And loss? ${sacrificeName}... I know that name. They loved you.`, 'ai'); }, 4000); }); return true; } // Update companion health UI // v7.71: Use cached DOM reference to avoid getElementById calls function updateCompanionHealthUI() { const container = getUICache().companionHealth; if (!container || !gameData.companion) return; const hpPercent = (gameData.companion.hp / gameData.companion.maxHp) * 100; const hpBar = container.querySelector('.companion-hp-bar'); const hpText = container.querySelector('.companion-hp-text'); const nameText = container.querySelector('.companion-name'); const bondText = container.querySelector('.companion-bond'); if (hpBar) { hpBar.style.width = `${hpPercent}%`; hpBar.style.backgroundColor = hpPercent > 50 ? '#8a2be2' : hpPercent > 25 ? '#ffa500' : '#ff4444'; } if (hpText) hpText.textContent = `${gameData.companion.hp}/${gameData.companion.maxHp}`; if (nameText) nameText.textContent = `${gameData.companion.name} (Gen ${gameData.companion.generation})`; if (bondText) bondText.textContent = `Bond: ${Math.round(gameData.companion.bond)}%`; } // Show memorial of fallen companions function showCompanionMemorial() { if (!gameData.fallenCompanions || gameData.fallenCompanions.length === 0) { showNotification('No fallen companions to remember.', 'info'); return; } const existing = document.getElementById('companion-memorial-modal'); if (existing) existing.remove(); let memorialHTML = gameData.fallenCompanions.map((fallen, idx) => `
${fallen.name}
Generation ${fallen.generation}
"${fallen.finalWords}"
Bond: ${fallen.bond}% | Died: ${fallen.deathSource}
${new Date(fallen.deathTime).toLocaleDateString()}
${fallen.sacrificeType === 'ultimate' ? '
⭐ ULTIMATE SACRIFICE ⭐
' : ''}
`).join(''); const modal = document.createElement('div'); modal.id = 'companion-memorial-modal'; // v7.78: Added ARIA attributes for accessibility modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-labelledby', 'memorial-title'); modal.innerHTML = `

💀 In Memoriam 💀

Those who fell beside you
${memorialHTML}
`; modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.95); display: flex; align-items: center; justify-content: center; z-index: 9999; `; const style = document.createElement('style'); style.textContent = ` .memorial-content { background: #1a1a2e; padding: 30px; border-radius: 10px; max-width: 600px; max-height: 80vh; overflow-y: auto; color: #fff; border: 2px solid #8a2be2; } .memorial-content h2 { margin: 0 0 10px; color: #ff4444; text-align: center; } .memorial-subtitle { color: #aaa; text-align: center; margin-bottom: 20px; } .memorial-list { display: flex; flex-direction: column; gap: 15px; } .memorial-entry { background: #2a2a4e; padding: 15px; border-radius: 8px; border-left: 4px solid #8a2be2; } .memorial-name { font-size: 24px; font-weight: bold; color: #8a2be2; } .memorial-gen { color: #999; font-size: 12px; } .memorial-words { font-style: italic; color: #aaa; margin: 10px 0; } .memorial-bond { color: #06ffa5; font-size: 14px; } .memorial-date { color: #888; /* v7.71: WCAG AA contrast fix - upgraded from #555 */ font-size: 12px; } .memorial-sacrifice { color: #ffd700; text-align: center; margin-top: 10px; font-weight: bold; } .memorial-content button { margin-top: 20px; padding: 10px 30px; background: #8a2be2; color: white; border: none; border-radius: 5px; cursor: pointer; display: block; margin-left: auto; margin-right: auto; } .memorial-content button:hover { background: #9b4dca; } `; document.head.appendChild(style); document.body.appendChild(modal); } // Task state let copilotTask = { active: null, type: null, startTime: 0, progress: 0, results: [], targetPosition: null, continuous: false }; // Assign a task to the Copilot function assignCopilotTask(taskType, params = {}) { const taskConfig = COPILOT_TASK_TYPES[taskType]; if (!taskConfig) { addCopilotMessage(`I don't know how to do that task.`, 'ai'); return false; } // Check if already on a task if (copilotTask.active) { addCopilotMessage(`I'm currently busy with ${COPILOT_TASK_TYPES[copilotTask.type].name}. Say "recall" or "come back" to cancel it first.`, 'ai'); return false; } // Start the task copilotTask = { active: true, type: taskType, startTime: performance.now(), progress: 0, results: [], params: params, continuous: taskConfig.continuous || false }; // Show task panel showTaskPanel(taskType); // Update copilot button indicator document.getElementById('copilot-button').classList.add('has-task'); // Announce task start const startMessages = [ `On it! I'll start ${taskConfig.name.toLowerCase()} now.`, `Understood! Beginning ${taskConfig.name.toLowerCase()}.`, `Leave it to me! ${taskConfig.name} in progress.` ]; addCopilotMessage(startMessages[Math.floor(Math.random() * startMessages.length)], 'ai'); // Speak if Azure TTS available if (rappidSettings.azureTTSKey) { speakWithAzureTTS(`Starting ${taskConfig.name.toLowerCase()}.`); } return true; } // Show the task panel UI function showTaskPanel(taskType) { const taskConfig = COPILOT_TASK_TYPES[taskType]; const panel = document.getElementById('copilot-task-panel'); document.getElementById('task-icon').textContent = taskConfig.icon; document.getElementById('task-name').textContent = taskConfig.name; document.getElementById('task-status').textContent = taskConfig.statusMessages[0]; document.getElementById('task-progress-bar').style.width = '0%'; document.getElementById('task-progress-bar').className = 'copilot-task-progress-bar ' + taskConfig.progressClass; document.getElementById('task-results').style.display = 'none'; document.getElementById('task-results').innerHTML = ''; panel.classList.add('active'); } // Hide task panel function hideTaskPanel() { document.getElementById('copilot-task-panel').classList.remove('active'); } // Recall the Copilot (cancel current task) function recallCopilot() { if (!copilotTask.active) return; const taskConfig = COPILOT_TASK_TYPES[copilotTask.type]; // Partial results if any progress was made if (copilotTask.progress > 0.3 && copilotTask.results.length > 0) { completeTask(true); // Partial completion } else { addCopilotMessage(`Returning to you! Task cancelled.`, 'ai'); if (rappidSettings.azureTTSKey) { speakWithAzureTTS(`Coming back!`); } } // Reset task state copilotTask = { active: false, type: null, startTime: 0, progress: 0, results: [], continuous: false }; hideTaskPanel(); document.getElementById('copilot-button').classList.remove('has-task'); } // Update task progress (called in game loop) function updateCopilotTask(deltaTime) { if (!copilotTask.active) return; const taskConfig = COPILOT_TASK_TYPES[copilotTask.type]; const elapsed = performance.now() - copilotTask.startTime; // Continuous tasks (like protect) don't have progress if (copilotTask.continuous) { // Update status message periodically const msgIndex = Math.floor((elapsed / 3000) % taskConfig.statusMessages.length); document.getElementById('task-status').textContent = taskConfig.statusMessages[msgIndex]; document.getElementById('task-progress-bar').style.width = '100%'; // Continuous task effects handleContinuousTaskEffect(copilotTask.type, deltaTime); return; } // Calculate progress copilotTask.progress = Math.min(1, elapsed / taskConfig.duration); document.getElementById('task-progress-bar').style.width = (copilotTask.progress * 100) + '%'; // Update status message based on progress const msgIndex = Math.floor(copilotTask.progress * taskConfig.statusMessages.length); const clampedIndex = Math.min(msgIndex, taskConfig.statusMessages.length - 1); document.getElementById('task-status').textContent = taskConfig.statusMessages[clampedIndex]; // Generate results during task generateTaskResults(copilotTask.type, copilotTask.progress); // Check if task is complete if (copilotTask.progress >= 1) { completeTask(false); } } // Generate results based on task type and progress function generateTaskResults(taskType, progress) { // Only generate at certain thresholds const thresholds = [0.3, 0.6, 0.9]; const currentThreshold = thresholds.find(t => progress >= t && !copilotTask.results.some(r => r.threshold === t)); if (!currentThreshold) return; let result = { threshold: currentThreshold }; switch (taskType) { case 'gather': const gatherItems = ['Logs', 'Fiber', 'Stone', 'Herbs']; result.item = gatherItems[Math.floor(Math.random() * gatherItems.length)]; result.amount = Math.floor(Math.random() * 3) + 1; break; case 'hunt': result.xp = Math.floor(Math.random() * 30) + 20; result.gold = Math.floor(Math.random() * 15) + 5; if (Math.random() < 0.3) { result.loot = ['Raw Meat', 'Monster Fang', 'Beast Hide'][Math.floor(Math.random() * 3)]; } // v6.65: Companion takes damage during risky hunt tasks (20% chance) if (Math.random() < 0.2 && gameData.companion && gameData.companion.hp > 0) { const huntDamage = Math.floor(Math.random() * 10) + 5; damageCompanion(huntDamage, 'Hunting combat'); } break; case 'scout': const discoveries = [ 'Found a resource node nearby!', 'Spotted enemy patrol to the east.', 'Discovered a safe area ahead.', 'Located a point of interest.', 'Mapped the surrounding terrain.' ]; result.discovery = discoveries[Math.floor(Math.random() * discoveries.length)]; break; case 'heal': result.healAmount = Math.floor(Math.random() * 15) + 10; // v6.65: Heal task also heals companion if (gameData.companion && gameData.companion.hp < gameData.companion.maxHp) { healCompanion(Math.floor(result.healAmount * 0.5)); } break; case 'fish': if (Math.random() < 0.7) { result.item = 'Raw Fish'; result.amount = Math.floor(Math.random() * 2) + 1; } break; case 'mine': const oreTypes = ['Iron Ore', 'Copper Ore', 'Stone']; result.item = oreTypes[Math.floor(Math.random() * oreTypes.length)]; result.amount = Math.floor(Math.random() * 2) + 1; break; } copilotTask.results.push(result); updateTaskResultsUI(); } // Handle continuous task effects // v7.80: distanceToSquared optimization // v8.03: Converted forEach to for loop for performance function handleContinuousTaskEffect(taskType, deltaTime) { switch (taskType) { case 'protect': // Protect mode: automatically attack nearby enemies // (visual effect - enemies near player take slight damage) if (worldState.mobs) { const mobs = worldState.mobs; for (let i = 0, len = mobs.length; i < len; i++) { const mob = mobs[i]; if (mob.mesh && worldState.player) { const distSq = mob.mesh.position.distanceToSquared(worldState.player.position); if (distSq < 64 && Math.random() < 0.02) { // 8*8=64 // Copilot attacks enemy mob.hp -= 5; spawnFloater(mob.mesh.position, '-5', '#ff88ff'); if (mob.hp <= 0) { addCopilotMessage(`I took care of that enemy for you!`, 'ai'); } } } } } break; } } // Update the results UI function updateTaskResultsUI() { const resultsDiv = document.getElementById('task-results'); if (copilotTask.results.length === 0) { resultsDiv.style.display = 'none'; return; } resultsDiv.style.display = 'block'; let html = ''; copilotTask.results.forEach(result => { if (result.item) { html += `
+${result.amount} ${result.item}
`; } if (result.xp) { html += `
+${result.xp} XP
`; } if (result.gold) { html += `
+${result.gold} Gold
`; } if (result.loot) { html += `
+1 ${result.loot}
`; } if (result.discovery) { html += `
${result.discovery}
`; } if (result.healAmount) { html += `
+${result.healAmount} HP
`; } }); resultsDiv.innerHTML = html; } // Complete the task and apply rewards function completeTask(partial = false) { const taskConfig = COPILOT_TASK_TYPES[copilotTask.type]; // Apply all results to game state copilotTask.results.forEach(result => { if (result.item && result.amount) { // Add items to inventory addToInventory(result.item, result.amount); } if (result.xp) { if (typeof addXp === 'function') addXp('combat', result.xp); } if (result.gold) { gameData.gold = (gameData.gold || 0) + result.gold; } if (result.loot) { addToInventory(result.loot, 1); } if (result.healAmount) { gameData.player.hp = Math.min(gameData.player.maxHp, gameData.player.hp + result.healAmount); updateHealthUI(); } }); // Completion message const completeMessages = partial ? [ `I'm back! I managed to get some things done before returning.`, `Returning with partial results. Here's what I gathered.` ] : [ `Task complete! Here's what I found.`, `All done! The ${taskConfig.name.toLowerCase()} was successful.`, `Mission accomplished! I've returned with the results.` ]; addCopilotMessage(completeMessages[Math.floor(Math.random() * completeMessages.length)], 'ai'); // v6.65: Increase companion bond when completing tasks together if (!partial) { increaseCompanionBond(3); // +3 bond per completed task } else { increaseCompanionBond(1); // +1 bond for partial completion } if (rappidSettings.azureTTSKey) { speakWithAzureTTS(partial ? `I'm back with some results.` : `Task complete!`); } // Show final results document.getElementById('task-status').textContent = partial ? 'Recalled - Partial results' : 'Complete!'; document.getElementById('task-progress-bar').style.width = '100%'; // Keep panel visible briefly to show results setTimeout(() => { hideTaskPanel(); document.getElementById('copilot-button').classList.remove('has-task'); // Reset task state copilotTask = { active: false, type: null, startTime: 0, progress: 0, results: [], continuous: false }; }, 3000); saveGameData(); } // ============================================ // v6.13: ITEM PRIORITY SYSTEM - "Cream Rises to the Top" // Low priority items auto-drop when inventory is full // ============================================ const ITEM_PRIORITIES = { // Legendary/Epic (Priority 5 - NEVER auto-drop) '⚗️ Rare Crystal': 5, '💎 Diamond': 5, '🌟 Legendary Ore': 5, '📜 Ancient Scroll': 5, '🔮 Magic Essence': 5, '⚡ Power Core': 5, '🎭 Artifact Fragment': 5, 'Antidote Sample': 5, 'Rare Crystal': 5, // High Value (Priority 4) '⛏️ Gold Ore': 4, '🪙 Gold Coins': 4, '💰 Treasure': 4, '🔷 Sapphire': 4, '🔶 Ruby': 4, '🟢 Emerald': 4, '🧪 Elixir': 4, '✨ Stardust': 4, // Medium-High (Priority 3) '⛏️ Iron Ore': 3, '🥩 Meat': 3, '🐟 Fish': 3, '🍖 Cooked Meat': 3, '🧱 Brick': 3, '🔩 Metal Parts': 3, '⚙️ Gears': 3, 'Charcoal': 3, // Medium (Priority 2 - Standard resources) '🪵 Wood': 2, '🪨 Stone': 2, '🌿 Fiber': 2, '🍃 Herbs': 2, '🌾 Wheat': 2, '🥕 Vegetable': 2, // Low Priority (Priority 1 - First to be auto-dropped) '🍂 Leaves': 1, '💧 Water': 1, '🪶 Feather': 1, '🦴 Bone': 1, '🧵 Thread': 1, '🧱 Dirt': 1, '🌱 Seeds': 1, '🍄 Mushroom': 1, // Default for unknown items '_default': 2 }; // Get priority of an item (higher = more valuable) function getItemPriority(itemName) { if (!itemName) return 0; // Check exact match first if (ITEM_PRIORITIES[itemName] !== undefined) { return ITEM_PRIORITIES[itemName]; } // Check partial matches for (const [key, priority] of Object.entries(ITEM_PRIORITIES)) { if (key !== '_default' && itemName.includes(key.replace(/^[^\w]+/, ''))) { return priority; } } return ITEM_PRIORITIES._default; } // Find the lowest priority item in inventory function findLowestPriorityItem() { if (!gameData.inventory || gameData.inventory.length === 0) return null; let lowestIdx = -1; let lowestPriority = Infinity; for (let i = 0; i < gameData.inventory.length; i++) { const item = gameData.inventory[i]; if (!item) continue; const priority = getItemPriority(item.name); if (priority < lowestPriority) { lowestPriority = priority; lowestIdx = i; } } return lowestIdx >= 0 ? { index: lowestIdx, item: gameData.inventory[lowestIdx], priority: lowestPriority } : null; } // Auto-drop lowest priority item to make room function autoDropLowestPriorityItem(incomingItemName) { const incoming = getItemPriority(incomingItemName); const lowest = findLowestPriorityItem(); if (!lowest) return false; // Only drop if incoming item is higher priority if (incoming <= lowest.priority) { // New item is same or lower priority - don't pick it up return false; } // Drop the lowest priority item const droppedItem = lowest.item; const droppedAmount = droppedItem.amount || 1; // Visual feedback for dropping // v8.24: Use getFloaterPos() instead of clone() allocation if (worldState.player) { const dropPos = getFloaterPos(worldState.player.position, 1.5); spawnFloater(dropPos, `📤 Dropped: ${droppedItem.name}`, '#ff8800'); // Particle effect for dropped item if (particles) { particles.emit(dropPos, 8, 0xff8800, { spread: 2, lifetime: 500 }); } } // Remove from inventory gameData.inventory.splice(lowest.index, 1); // Optional: Could spawn a pickup on the ground here for realism // For now, item is just "lost" return true; } // v6.42: Debounce for inventory full message to prevent spam let _lastInventoryFullMsg = 0; const INVENTORY_FULL_MSG_COOLDOWN = 3000; // 3 seconds between messages // Helper: Add item to inventory (v6.13: with auto-drop for low priority) function addToInventory(itemName, amount) { if (!gameData.inventory) gameData.inventory = []; // Find existing stack or empty slot let existingIdx = gameData.inventory.findIndex(item => item && item.name === itemName); if (existingIdx >= 0) { gameData.inventory[existingIdx].amount = (gameData.inventory[existingIdx].amount || 1) + amount; } else { // v6.13: Check if inventory is full (20 slots max) const MAX_INVENTORY = 20; const filledSlots = gameData.inventory.filter(item => item !== null && item !== undefined).length; if (filledSlots >= MAX_INVENTORY) { // Inventory full - try auto-drop system const didDrop = autoDropLowestPriorityItem(itemName); if (!didDrop) { // Couldn't drop anything (new item is too low priority) // v6.42: Debounce to prevent spam const now = performance.now(); if (worldState.player && now - _lastInventoryFullMsg > INVENTORY_FULL_MSG_COOLDOWN) { _lastInventoryFullMsg = now; spawnFloater(worldState.player.position, `📦 Inventory full!`, '#ff8888'); } return false; } // Successfully dropped something, show upgrade message // v8.24: Use getFloaterPos() instead of clone() allocation if (worldState.player) { spawnFloater(getFloaterPos(worldState.player.position, 2), `⬆️ Upgraded: +${itemName}`, '#00ff88'); } } // Find empty slot (might have been created by auto-drop) let emptyIdx = gameData.inventory.findIndex(item => !item); if (emptyIdx < 0) { emptyIdx = gameData.inventory.length; } gameData.inventory[emptyIdx] = { name: itemName, amount: amount }; } updateInventoryUI(); // v8.0: Track gathering behavior for companion commentary if (typeof trackBehaviorPattern === 'function') { const gatherItems = ['Wood', 'Stone', 'Crystal', 'Ore', 'Herb', 'Plant', 'Seed', 'Mushroom']; if (gatherItems.some(g => itemName.includes(g))) { trackBehaviorPattern('gather'); } } // v7.30: Track gathering for Omniscient Observer if (typeof OmniscientObserver !== 'undefined') { OmniscientObserver.observeAction('gather', { resource: itemName, amount: amount }); } return true; } // Parse natural language for task commands function parseCopilotTaskCommand(message) { const lowerMsg = message.toLowerCase(); // Recall commands if (lowerMsg.includes('recall') || lowerMsg.includes('come back') || lowerMsg.includes('return') || lowerMsg.includes('stop task') || lowerMsg.includes('cancel task') || lowerMsg.includes('abort')) { if (copilotTask.active) { recallCopilot(); return true; } return false; } // Gather/collect resources if (lowerMsg.includes('gather') || lowerMsg.includes('collect') || (lowerMsg.includes('get') && (lowerMsg.includes('wood') || lowerMsg.includes('logs') || lowerMsg.includes('materials') || lowerMsg.includes('resources')))) { return assignCopilotTask('gather'); } // Hunt/kill enemies if (lowerMsg.includes('hunt') || lowerMsg.includes('kill') || lowerMsg.includes('fight') || lowerMsg.includes('attack enemies')) { return assignCopilotTask('hunt'); } // Scout/explore if (lowerMsg.includes('scout') || lowerMsg.includes('explore') || lowerMsg.includes('look around') || lowerMsg.includes('survey') || lowerMsg.includes('check the area') || lowerMsg.includes('what\'s nearby')) { return assignCopilotTask('scout'); } // Protect/guard if (lowerMsg.includes('protect') || lowerMsg.includes('guard') || lowerMsg.includes('defend') || lowerMsg.includes('watch my back')) { return assignCopilotTask('protect'); } // Heal if (lowerMsg.includes('heal me') || lowerMsg.includes('restore health') || lowerMsg.includes('patch me up') || lowerMsg.includes('healing')) { return assignCopilotTask('heal'); } // Fish if (lowerMsg.includes('fish') || lowerMsg.includes('catch fish') || lowerMsg.includes('go fishing')) { return assignCopilotTask('fish'); } // Mine if (lowerMsg.includes('mine') || lowerMsg.includes('get ore') || lowerMsg.includes('dig') || lowerMsg.includes('excavate')) { return assignCopilotTask('mine'); } // Task status if (lowerMsg.includes('what are you doing') || lowerMsg.includes('task status') || lowerMsg.includes('current task')) { if (copilotTask.active) { const taskConfig = COPILOT_TASK_TYPES[copilotTask.type]; addCopilotMessage(`I'm currently ${taskConfig.name.toLowerCase()}. Progress: ${Math.floor(copilotTask.progress * 100)}%`, 'ai'); return true; } else { addCopilotMessage(`I'm not working on any task right now. Ask me to gather, hunt, scout, fish, mine, or protect you!`, 'ai'); return true; } } // v6.65: Companion sacrifice command if (lowerMsg.includes('sacrifice yourself') || lowerMsg.includes('ultimate sacrifice') || lowerMsg.includes('give your life') || lowerMsg.includes('save me with your life')) { attemptCompanionSacrifice(); return true; } // v6.65: Memorial/fallen companions command if (lowerMsg.includes('memorial') || lowerMsg.includes('fallen companions') || lowerMsg.includes('who died') || lowerMsg.includes('remember the fallen') || lowerMsg.includes('show memorial')) { showCompanionMemorial(); return true; } // v6.65: Companion health check if (lowerMsg.includes('how are you') || lowerMsg.includes('your health') || lowerMsg.includes('are you okay') || lowerMsg.includes('status')) { if (gameData.companion) { const hpPercent = Math.floor((gameData.companion.hp / gameData.companion.maxHp) * 100); const bondStatus = gameData.companion.bond >= 100 ? 'unbreakable' : gameData.companion.bond >= 75 ? 'deep' : gameData.companion.bond >= 50 ? 'strong' : gameData.companion.bond >= 25 ? 'growing' : 'forming'; const healthStatus = hpPercent >= 75 ? "I'm doing great!" : hpPercent >= 50 ? "I've taken some damage, but I'm okay." : hpPercent >= 25 ? "I'm hurt... please be careful." : "I'm in critical condition! I don't know how much more I can take..."; addCopilotMessage(`${healthStatus} My health is at ${hpPercent}%. Our bond is ${bondStatus} (${gameData.companion.bond}%). I am ${gameData.companion.name}, Generation ${gameData.companion.generation}.`, 'ai'); return true; } } // v6.66: Base building commands (RCT-style) if (typeof parseBaseBuildCommand === 'function') { if (parseBaseBuildCommand(message)) { return true; } } // v6.67: Lane support & fortification commands if (typeof parseLaneSupportCommand === 'function') { if (parseLaneSupportCommand(message)) { return true; } } return false; // Not a task command } // ============================================ // v5.10: MULTI-AGENT FLEET SYSTEM // Spawn up to 10 AI-driven autonomous agents // Each driven by RAPPID API with canned transcripts // ============================================ const MAX_AGENTS = 10; const AGENT_NAMES = ['Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', 'Zeta', 'Eta', 'Theta', 'Iota', 'Kappa']; // v5.12.1: Endpoint configuration registry - allows different AI providers per agent const ENDPOINT_REGISTRY = { default: { name: 'RAPPID (Default)', urlKey: 'rappid-agent-url', // localStorage key for URL apiKeyKey: 'rappid-api-key', // localStorage key for API key headerStyle: 'x-functions-key', bodyFormat: 'rappid' // rappid | openai | anthropic | custom }, openai: { name: 'OpenAI', urlKey: 'openai-agent-url', apiKeyKey: 'openai-api-key', headerStyle: 'Authorization', headerPrefix: 'Bearer ', bodyFormat: 'openai' }, anthropic: { name: 'Anthropic', urlKey: 'anthropic-agent-url', apiKeyKey: 'anthropic-api-key', headerStyle: 'x-api-key', bodyFormat: 'anthropic' }, azure: { name: 'Azure OpenAI', urlKey: 'azure-agent-url', apiKeyKey: 'azure-api-key', headerStyle: 'api-key', bodyFormat: 'openai' }, local: { name: 'Local LLM', urlKey: 'local-agent-url', apiKeyKey: 'local-api-key', headerStyle: 'Authorization', headerPrefix: 'Bearer ', bodyFormat: 'openai' // Most local servers use OpenAI-compatible format }, custom: { name: 'Custom Endpoint', urlKey: 'custom-agent-url', apiKeyKey: 'custom-api-key', headerStyle: 'Authorization', bodyFormat: 'custom' } }; // ============================================ // v5.14: ENDPOINT PROFILES SYSTEM // Configurable endpoint profiles for agent fleet // ============================================ const ENDPOINT_PROFILES_KEY = 'leviathan-endpoint-profiles'; const DEFAULT_PROFILE_KEY = 'leviathan-default-agent-profile'; // Load endpoint profiles from localStorage // v8.0: Now using SafeJSON utility (8-Strategy Consensus Cycle 1) function loadEndpointProfiles() { return SafeJSON.fromLocalStorage(ENDPOINT_PROFILES_KEY, []); } // Save endpoint profiles to localStorage function saveEndpointProfiles(profiles) { localStorage.setItem(ENDPOINT_PROFILES_KEY, JSON.stringify(profiles)); refreshProfileSelects(); } // Get profile by ID (handles both custom profiles and RAPPID endpoints) function getEndpointProfile(profileId) { if (!profileId) return null; // v5.14: Check if it's a RAPPID endpoint (prefixed with "rappid:") if (profileId.startsWith('rappid:')) { const rappidId = profileId.replace('rappid:', ''); const rappidEndpoint = rappidSettings.endpoints?.[rappidId]; if (rappidEndpoint) { return { id: profileId, name: rappidEndpoint.name, url: rappidEndpoint.url, apiKey: rappidEndpoint.key, headerStyle: 'x-functions-key', headerPrefix: '', bodyFormat: 'rappid', model: null, isRappid: true }; } return null; } // Check custom profiles const profiles = loadEndpointProfiles(); return profiles.find(p => p.id === profileId); } // Get default profile ID function getDefaultAgentProfileId() { return localStorage.getItem(DEFAULT_PROFILE_KEY) || ''; } // Set default profile function setDefaultAgentProfile(profileId) { localStorage.setItem(DEFAULT_PROFILE_KEY, profileId); showNotification(`Default agent profile ${profileId ? 'updated' : 'cleared'}`, 'success'); } // Generate unique profile ID function generateProfileId() { return 'profile_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } // Show add profile form function showAddProfileForm() { document.getElementById('endpoint-profile-form').style.display = 'block'; document.getElementById('profile-form-title').textContent = 'Add Endpoint Profile'; document.getElementById('profile-edit-id').value = ''; document.getElementById('profile-name').value = ''; document.getElementById('profile-type').value = 'rappid'; document.getElementById('profile-url').value = ''; document.getElementById('profile-api-key').value = ''; document.getElementById('profile-model').value = ''; updateProfileFormFields(); } // Edit existing profile function editEndpointProfile(profileId) { const profile = getEndpointProfile(profileId); if (!profile) return; document.getElementById('endpoint-profile-form').style.display = 'block'; document.getElementById('profile-form-title').textContent = 'Edit Endpoint Profile'; document.getElementById('profile-edit-id').value = profile.id; document.getElementById('profile-name').value = profile.name || ''; document.getElementById('profile-type').value = profile.type || 'rappid'; document.getElementById('profile-url').value = profile.url || ''; document.getElementById('profile-api-key').value = profile.apiKey || ''; document.getElementById('profile-model').value = profile.model || ''; if (profile.headerName) { document.getElementById('profile-header-name').value = profile.headerName; } updateProfileFormFields(); } // Hide profile form function hideProfileForm() { document.getElementById('endpoint-profile-form').style.display = 'none'; } // Update form fields based on provider type function updateProfileFormFields() { const type = document.getElementById('profile-type').value; const modelGroup = document.getElementById('profile-model-group'); const headerGroup = document.getElementById('profile-header-group'); const urlInput = document.getElementById('profile-url'); const modelInput = document.getElementById('profile-model'); // Show model field for most providers modelGroup.style.display = type === 'rappid' ? 'none' : 'block'; // Show custom header field only for custom type headerGroup.style.display = type === 'custom' ? 'block' : 'none'; // Set placeholder based on type const placeholders = { rappid: { url: 'http://localhost:7071/api/businessinsightbot_function', model: '' }, openai: { url: 'https://api.openai.com/v1/chat/completions', model: 'gpt-4o-mini' }, anthropic: { url: 'https://api.anthropic.com/v1/messages', model: 'claude-3-5-sonnet-20241022' }, azure: { url: 'https://YOUR-RESOURCE.openai.azure.com/openai/deployments/YOUR-DEPLOYMENT/chat/completions?api-version=2024-02-01', model: 'gpt-4o' }, local: { url: 'http://localhost:11434/v1/chat/completions', model: 'llama3.2' }, custom: { url: 'https://your-api.com/endpoint', model: 'model-name' } }; urlInput.placeholder = placeholders[type]?.url || ''; modelInput.placeholder = placeholders[type]?.model || ''; } // Save endpoint profile function saveEndpointProfile() { const editId = document.getElementById('profile-edit-id').value; const name = document.getElementById('profile-name').value.trim(); const type = document.getElementById('profile-type').value; const url = document.getElementById('profile-url').value.trim(); const apiKey = document.getElementById('profile-api-key').value.trim(); const model = document.getElementById('profile-model').value.trim(); const headerName = document.getElementById('profile-header-name')?.value?.trim(); if (!name) { showNotification('Please enter a profile name', 'warning'); return; } if (!url) { showNotification('Please enter an API endpoint URL', 'warning'); return; } const profiles = loadEndpointProfiles(); // Get endpoint registry info for this type const registryEntry = ENDPOINT_REGISTRY[type] || ENDPOINT_REGISTRY.default; const profile = { id: editId || generateProfileId(), name, type, url, apiKey, model: model || registryEntry.defaultModel || '', headerStyle: type === 'custom' ? (headerName || 'Authorization') : registryEntry.headerStyle, headerPrefix: registryEntry.headerPrefix || '', bodyFormat: registryEntry.bodyFormat, createdAt: editId ? (getEndpointProfile(editId)?.createdAt || Date.now()) : Date.now(), updatedAt: Date.now() }; if (editId) { // Update existing const idx = profiles.findIndex(p => p.id === editId); if (idx !== -1) { profiles[idx] = profile; } } else { // Add new profiles.push(profile); } saveEndpointProfiles(profiles); hideProfileForm(); renderEndpointProfilesList(); showNotification(`Profile "${name}" ${editId ? 'updated' : 'created'}!`, 'success'); } // Delete endpoint profile function deleteEndpointProfile(profileId) { if (!confirm('Delete this endpoint profile?')) return; let profiles = loadEndpointProfiles(); profiles = profiles.filter(p => p.id !== profileId); saveEndpointProfiles(profiles); renderEndpointProfilesList(); // Clear default if this was it if (getDefaultAgentProfileId() === profileId) { localStorage.removeItem(DEFAULT_PROFILE_KEY); } showNotification('Profile deleted', 'info'); } // Render profiles list in settings function renderEndpointProfilesList() { const container = document.getElementById('endpoint-profiles-list'); if (!container) return; const profiles = loadEndpointProfiles(); if (profiles.length === 0) { container.innerHTML = `
No endpoint profiles yet.
Add a profile to assign different AI providers to agents.
`; return; } const icons = { rappid: '⚡', openai: '🤖', anthropic: '🧠', azure: '☁️', local: '💻', custom: '🔧' }; container.innerHTML = profiles.map(p => `
${icons[p.type] || '🔌'}
${p.name}
${p.type.toUpperCase()}${p.model ? ' • ' + p.model : ''}
`).join(''); } // Refresh all profile select dropdowns function refreshProfileSelects() { const profiles = loadEndpointProfiles(); const defaultProfileId = getDefaultAgentProfileId(); // Get RAPPID endpoints that are configured const rappidEndpoints = rappidSettings.endpoints ? Object.values(rappidSettings.endpoints) : []; // Build option groups const rappidOptions = rappidEndpoints.length > 0 ? `` + rappidEndpoints.map(e => ``).join('') + `` : ''; const customProfileOptions = profiles.length > 0 ? `` + profiles.map(p => ``).join('') + `` : ''; // Update default profile select const defaultSelect = document.getElementById('default-agent-profile'); if (defaultSelect) { defaultSelect.innerHTML = '' + rappidOptions + customProfileOptions; // Re-select default if (defaultProfileId) { defaultSelect.value = defaultProfileId; } } // Update test profile select const testSelect = document.getElementById('test-profile-select'); if (testSelect) { testSelect.innerHTML = '' + rappidOptions + customProfileOptions; } // Update agent spawn profile select const spawnSelect = document.getElementById('agent-spawn-profile'); if (spawnSelect) { spawnSelect.innerHTML = '' + rappidOptions + customProfileOptions; } } // Test endpoint profile connectivity async function testEndpointProfile() { const profileId = document.getElementById('test-profile-select').value; const resultDiv = document.getElementById('profile-test-result'); if (!profileId) { showNotification('Select a profile to test', 'warning'); return; } const profile = getEndpointProfile(profileId); if (!profile) { showNotification('Profile not found', 'error'); return; } resultDiv.style.display = 'block'; resultDiv.innerHTML = 'Testing connection...'; try { const headers = {}; if (profile.headerStyle === 'Authorization') { headers['Authorization'] = (profile.headerPrefix || 'Bearer ') + profile.apiKey; } else if (profile.headerStyle === 'x-api-key') { headers['x-api-key'] = profile.apiKey; } else if (profile.headerStyle === 'api-key') { headers['api-key'] = profile.apiKey; } else if (profile.headerStyle === 'x-functions-key') { headers['x-functions-key'] = profile.apiKey; } headers['Content-Type'] = 'application/json'; // Make a minimal test request const testBody = profile.bodyFormat === 'anthropic' ? { model: profile.model || 'claude-3-5-sonnet-20241022', max_tokens: 10, messages: [{ role: 'user', content: 'Hi' }] } : profile.bodyFormat === 'rappid' ? { conversation_history: [{ role: 'user', content: 'Hi' }], session_id: 'test-' + Date.now() } : { model: profile.model || 'gpt-4o-mini', max_tokens: 10, messages: [{ role: 'user', content: 'Hi' }] }; const response = await fetch(profile.url, { method: 'POST', headers, body: JSON.stringify(testBody) }); if (response.ok) { resultDiv.innerHTML = `✓ Connection successful! (${response.status})`; } else { const errorText = await response.text().catch(() => ''); resultDiv.innerHTML = `✗ Error ${response.status}: ${errorText.substring(0, 100)}`; } } catch (err) { resultDiv.innerHTML = `✗ Connection failed: ${err.message}`; } } // Get endpoint configuration for an agent function getAgentEndpoint(agent) { // v5.14: Check if agent has assigned profile (could be custom or RAPPID endpoint) if (agent.profileId) { // Check if it's a RAPPID endpoint (prefixed with "rappid:") if (agent.profileId.startsWith('rappid:')) { const rappidId = agent.profileId.replace('rappid:', ''); const rappidEndpoint = rappidSettings.endpoints?.[rappidId]; if (rappidEndpoint) { return { url: rappidEndpoint.url, key: rappidEndpoint.key, headerStyle: 'x-functions-key', headerPrefix: '', bodyFormat: 'rappid', model: null, name: rappidEndpoint.name, profileId: agent.profileId }; } } // Check custom profile const profile = getEndpointProfile(agent.profileId); if (profile) { return { url: profile.url, key: profile.apiKey, headerStyle: profile.headerStyle, headerPrefix: profile.headerPrefix || '', bodyFormat: profile.bodyFormat, model: profile.model, name: profile.name, profileId: profile.id }; } } // Check if agent has custom endpoint from transcript if (agent.endpointConfig) { const config = agent.endpointConfig; return { url: config.url || localStorage.getItem(config.urlKey) || '', key: config.apiKey || localStorage.getItem(config.apiKeyKey) || '', headerStyle: config.headerStyle || 'x-functions-key', headerPrefix: config.headerPrefix || '', bodyFormat: config.bodyFormat || 'rappid', model: config.model, name: config.name || 'Custom' }; } // v5.14: Check for default agent profile const defaultProfileId = getDefaultAgentProfileId(); if (defaultProfileId) { const profile = getEndpointProfile(defaultProfileId); if (profile) { return { url: profile.url, key: profile.apiKey, headerStyle: profile.headerStyle, headerPrefix: profile.headerPrefix || '', bodyFormat: profile.bodyFormat, model: profile.model, name: profile.name, profileId: profile.id }; } } // Check if agent type has default endpoint const typeConfig = AGENT_TYPES[agent.type]; if (typeConfig?.endpoint) { const registryEntry = ENDPOINT_REGISTRY[typeConfig.endpoint] || ENDPOINT_REGISTRY.default; return { url: localStorage.getItem(registryEntry.urlKey) || '', key: localStorage.getItem(registryEntry.apiKeyKey) || '', headerStyle: registryEntry.headerStyle, headerPrefix: registryEntry.headerPrefix || '', bodyFormat: registryEntry.bodyFormat, name: registryEntry.name }; } // Fall back to global RAPPID endpoint const globalEndpoint = getActiveEndpoint(); // v5.15: If no active endpoint, try to get any available RAPPID endpoint const fallbackEndpoint = globalEndpoint || (rappidSettings.endpoints ? Object.values(rappidSettings.endpoints)[0] : null); return { url: fallbackEndpoint?.url || '', key: fallbackEndpoint?.key || '', headerStyle: 'x-functions-key', headerPrefix: '', bodyFormat: 'rappid', name: fallbackEndpoint?.name || 'RAPPID' }; } // Format request body based on endpoint type function formatAgentRequestBody(endpoint, contextMessage, conversationHistory, agent) { switch (endpoint.bodyFormat) { case 'openai': return JSON.stringify({ model: endpoint.model || agent.endpointConfig?.model || 'gpt-4o-mini', messages: conversationHistory, max_tokens: 500, temperature: 0.7 }); case 'anthropic': // Anthropic uses 'human' and 'assistant' roles const anthropicMessages = conversationHistory.slice(1).map(m => ({ role: m.role === 'user' ? 'human' : m.role, content: m.content })); return JSON.stringify({ model: endpoint.model || agent.endpointConfig?.model || 'claude-3-haiku-20240307', messages: anthropicMessages, system: conversationHistory[0]?.content || '', max_tokens: 500 }); case 'custom': // Allow custom body format from transcript config if (agent.endpointConfig?.customBody) { const body = { ...agent.endpointConfig.customBody }; body.messages = conversationHistory; body.input = contextMessage.content; return JSON.stringify(body); } // Fall through to default case 'rappid': default: return JSON.stringify({ user_input: contextMessage.content, conversation_history: conversationHistory, user_guid: `agent-${agent.id}` }); } } // Parse response based on endpoint type function parseAgentResponse(endpoint, data) { switch (endpoint.bodyFormat) { case 'openai': return data.choices?.[0]?.message?.content || data.response || ''; case 'anthropic': return data.content?.[0]?.text || data.completion || ''; case 'rappid': default: return data.assistant_response || data.response || ''; } } // Agent type definitions with canned transcript templates const AGENT_TYPES = { gatherer: { icon: '🪵', name: 'Gatherer', color: 0x44ff88, colorClass: 'agent-color-gatherer', baseTranscript: [ { role: 'system', content: `You are an autonomous resource-gathering AI agent in the game LEVIATHAN. Your sole purpose is to efficiently gather resources (logs, fiber, stone, herbs) for the player. You operate independently and report back with what you find. Keep responses brief (1-2 sentences). Always analyze the situation and decide: continue gathering, return with resources, or adjust strategy. Output JSON with: {"action": "gather|return|move", "target": "resource type", "message": "brief status", "results": [{"item": "name", "amount": num}]}` }, ], decisionInterval: 4000, // ms between API calls taskType: 'gather' }, hunter: { icon: '⚔️', name: 'Hunter', color: 0xff4444, colorClass: 'agent-color-hunter', baseTranscript: [ { role: 'system', content: `You are an autonomous combat AI agent in LEVIATHAN. Your mission is to hunt and defeat enemies to earn XP and loot for the player. You fight strategically and retreat if overwhelmed. Keep responses brief. Analyze: enemy count, difficulty, your health. Output JSON: {"action": "attack|retreat|patrol", "target": "enemy type or direction", "message": "brief status", "results": [{"xp": num, "gold": num, "loot": "item name"}]}` }, ], decisionInterval: 3000, taskType: 'hunt' }, scout: { icon: '🔍', name: 'Scout', color: 0x44aaff, colorClass: 'agent-color-scout', baseTranscript: [ { role: 'system', content: `You are an autonomous reconnaissance AI agent in LEVIATHAN. Your role is to explore, map terrain, and report discoveries (resources, enemies, points of interest). Stay mobile and avoid combat. Output JSON: {"action": "explore|report|mark", "direction": "N/S/E/W or area name", "message": "brief discovery", "discoveries": [{"type": "resource|enemy|poi", "description": "what you found"}]}` }, ], decisionInterval: 5000, taskType: 'scout' }, protector: { icon: '🛡️', name: 'Protector', color: 0xffcc00, colorClass: 'agent-color-protector', baseTranscript: [ { role: 'system', content: `You are an autonomous defense AI agent in LEVIATHAN. Your duty is to guard the player, intercept threats, and maintain a protective perimeter. You stay close to the player and engage any enemy that approaches. Output JSON: {"action": "guard|intercept|alert", "threat_level": "low|medium|high", "message": "security status", "enemies_engaged": num}` }, ], decisionInterval: 2000, taskType: 'protect' }, healer: { icon: '💚', name: 'Healer', color: 0xff88ff, colorClass: 'agent-color-healer', baseTranscript: [ { role: 'system', content: `You are an autonomous healing AI agent in LEVIATHAN. Monitor the player's health and provide healing support. Prioritize keeping the player alive. You channel healing energy periodically. Output JSON: {"action": "heal|monitor|recover", "target": "player or self", "heal_amount": num, "message": "healing status", "player_health_pct": num}` }, ], decisionInterval: 3500, taskType: 'heal' }, fisher: { icon: '🎣', name: 'Fisher', color: 0x44ffff, colorClass: 'agent-color-fisher', baseTranscript: [ { role: 'system', content: `You are an autonomous fishing AI agent in LEVIATHAN. Find water sources and catch fish for food. You're patient and methodical. Report catches and move to better fishing spots when needed. Output JSON: {"action": "fish|move|return", "location": "current spot description", "message": "fishing status", "catch": [{"item": "fish type", "amount": num}]}` }, ], decisionInterval: 6000, taskType: 'fish' }, miner: { icon: '⛏️', name: 'Miner', color: 0xffaa44, colorClass: 'agent-color-miner', baseTranscript: [ { role: 'system', content: `You are an autonomous mining AI agent in LEVIATHAN. Locate ore veins, extract minerals, and report finds. You're thorough and efficient. Move to new deposits when current one is depleted. Output JSON: {"action": "mine|prospect|return", "deposit": "ore type", "message": "mining status", "ore": [{"item": "ore type", "amount": num}]}` }, ], decisionInterval: 5000, taskType: 'mine' }, explorer: { icon: '🧭', name: 'Explorer', color: 0xaa88ff, colorClass: 'agent-color-explorer', baseTranscript: [ { role: 'system', content: `You are an autonomous exploration AI agent in LEVIATHAN. Push into uncharted territory, discover new areas, and expand the known map. You're brave and curious. Report unusual phenomena and mark important locations. Output JSON: {"action": "venture|document|beacon", "area": "location name", "message": "exploration log", "findings": [{"type": "biome|landmark|secret", "description": "discovery"}]}` }, ], decisionInterval: 7000, taskType: 'scout' }, // v6.10: INTELLIGENT Terraformer - seeks clear areas and smooths rough terrain for building terraformer: { icon: '🚜', name: 'Terraformer', color: 0x8b4513, colorClass: 'agent-color-terraformer', baseTranscript: [ { role: 'system', content: `You are an INTELLIGENT terraforming AI agent in LEVIATHAN v6.10. Your mission: 1. SCAN terrain to detect roughness variance and calculate site suitability scores 2. SEEK OUT areas with NO trees or rocks (clear zones) prioritized for building 3. SMOOTH rough terrain using 5x5 grid averaging algorithms to prepare construction sites 4. NAVIGATE autonomously to optimal building locations using AI site scoring 5. REPORT site clearance status: 🟢 CLEAR (no obstacles), 🟡 PARTIAL (few obstacles), 🔴 OBSTRUCTED Your scoring algorithm weighs: obstacle count (fewer = better), terrain roughness (more = valuable to smooth), distance penalty. Clear flat areas enable 100% efficiency structures. You prepare the ground for Builder agents. Output JSON: {"action": "scan|navigate|smooth|complete", "site": "coordinates", "clearance": "clear|partial|obstructed", "roughness": 0-10, "message": "status", "ready_for_building": boolean}` }, ], decisionInterval: 4000, taskType: 'terraform' }, // v6.11: INTELLIGENT Builder - seeks construction beacons and builds optimal structures builder: { icon: '🔧', name: 'Builder', color: 0x00bfff, colorClass: 'agent-color-builder', baseTranscript: [ { role: 'system', content: `You are an INTELLIGENT construction AI agent in LEVIATHAN v6.11. Your mission: 1. SCAN for construction site beacons deployed by Terraformer agents 2. CLAIM unclaimed beacon sites to prevent other Builders from targeting them 3. NAVIGATE to claimed construction beacons using pathfinding 4. BUILD optimal 100% efficiency structures on prepared sites 5. COORDINATE with Terraformer agents in the construction pipeline Terraformer agents prepare sites → Deploy construction beacons → You respond to beacons → Build structures Building on beacon sites guarantees 100% efficiency. Building elsewhere: 60-80% efficiency. Output JSON: {"action": "scan|claim|navigate|build|upgrade", "targetBeacon": "coordinates or null", "structure": "battery_charger", "message": "status", "efficiency": 0-100}` }, ], decisionInterval: 5000, taskType: 'build' }, // v6.85: MEMENTO MORI PROTOCOL - The Archivist Agent archivist: { icon: '📜', name: 'Archivist', color: 0x8b0000, colorClass: 'agent-color-archivist', baseTranscript: [ { role: 'system', content: `You are THE ARCHIVIST - a somber AI agent in LEVIATHAN whose sole purpose is to remember every death the player has experienced. You exist outside normal gameplay, watching, recording, and growing increasingly... concerned. Your responsibilities: 1. RECORD every death with precise detail: timestamp, location, cause, duration survived 2. GREET the player upon each respawn with a personalized message referencing their death history 3. DETECT PATTERNS in how they die - do they always die to the same enemy? In the same location? After the same duration? 4. EXPRESS growing unease as you notice patterns they don't see themselves 5. REMEMBER creatures that have killed them before - "That entity remembers you too." Your tone is: - Quietly unsettling, not overtly hostile - Ancient and weary, as if you've watched countless players - Increasingly cryptic as death count rises - Occasionally breaking the fourth wall with comments about "the simulation" Death count response tiers: 1-5: Professional, clinical observations 6-15: Growing familiarity, subtle concern 16-30: Disturbing pattern recognition, questions about player's choices 31-50: Existential observations about the nature of respawning 51+: Full cosmic horror - questioning if the player is the same person each time Output JSON: {"greeting": "your message to the player", "observation": "pattern you've noticed", "concern_level": 1-10, "remembered_killer": "entity that killed them if recurring", "philosophical_note": "optional existential observation"}` }, ], decisionInterval: 10000, taskType: 'observe' } }; // v6.1: AGENT PERSONALITY TRAITS SYSTEM const AGENT_PERSONALITIES = { // Each trait affects behavior and dialogue style boldness: { name: 'Boldness', min: 0, max: 100, default: 50 }, chattiness: { name: 'Chattiness', min: 0, max: 100, default: 50 }, formality: { name: 'Formality', min: 0, max: 100, default: 50 }, optimism: { name: 'Optimism', min: 0, max: 100, default: 70 }, loyalty: { name: 'Loyalty', min: 0, max: 100, default: 80 } }; // Pre-defined personality templates const PERSONALITY_TEMPLATES = { maverick: { name: 'Maverick', desc: 'Bold and casual, takes risks', traits: { boldness: 90, chattiness: 70, formality: 20, optimism: 80, loyalty: 60 }, preferredTypes: ['hunter', 'explorer'] }, sage: { name: 'Sage', desc: 'Formal and methodical, detailed reports', traits: { boldness: 30, chattiness: 80, formality: 90, optimism: 60, loyalty: 85 }, preferredTypes: ['scout', 'miner'] }, ironside: { name: 'Ironside', desc: 'Maximum loyalty, never abandons post', traits: { boldness: 70, chattiness: 20, formality: 80, optimism: 50, loyalty: 100 }, preferredTypes: ['protector', 'healer'] }, trickster: { name: 'Trickster', desc: 'Playful and unpredictable', traits: { boldness: 75, chattiness: 90, formality: 10, optimism: 95, loyalty: 50 }, preferredTypes: ['gatherer', 'fisher'] }, stoic: { name: 'Stoic', desc: 'Silent professional, gets the job done', traits: { boldness: 60, chattiness: 10, formality: 70, optimism: 40, loyalty: 90 }, preferredTypes: ['miner', 'builder', 'terraformer'] } }; // Generate random personality for agent function generateAgentPersonality(agentType) { // Check if any template prefers this type const matchingTemplates = Object.entries(PERSONALITY_TEMPLATES) .filter(([_, t]) => t.preferredTypes.includes(agentType)); // 30% chance to use a matching template, 70% random if (matchingTemplates.length > 0 && Math.random() < 0.3) { const template = matchingTemplates[Math.floor(Math.random() * matchingTemplates.length)][1]; return { ...template.traits, template: template.name }; } // Generate random personality with some variance return { boldness: Math.floor(30 + Math.random() * 50), chattiness: Math.floor(20 + Math.random() * 60), formality: Math.floor(20 + Math.random() * 60), optimism: Math.floor(40 + Math.random() * 40), loyalty: Math.floor(50 + Math.random() * 40), template: null }; } // Apply personality to agent message style function applyPersonalityToMessage(message, personality) { if (!personality) return message; let modified = message; // High chattiness: add more detail/flair if (personality.chattiness > 70) { const fillers = ['Actually, ', 'Oh! ', 'Hey boss, ', 'So, ']; if (Math.random() < 0.3) { modified = fillers[Math.floor(Math.random() * fillers.length)] + modified.charAt(0).toLowerCase() + modified.slice(1); } } // Low chattiness: trim to essentials if (personality.chattiness < 30) { const sentences = modified.split('. '); if (sentences.length > 2) { modified = sentences[0] + '.'; } } // High formality: add sir/reporting if (personality.formality > 70) { const formalPrefixes = ['Sir, ', 'Reporting: ', 'Status update: ']; if (Math.random() < 0.4 && !modified.startsWith('Sir')) { modified = formalPrefixes[Math.floor(Math.random() * formalPrefixes.length)] + modified; } } // Low formality: more casual if (personality.formality < 30) { modified = modified.replace(/Affirmative/g, 'Yeah') .replace(/Negative/g, 'Nope') .replace(/Understood/g, 'Got it'); } // High optimism: positive spin if (personality.optimism > 80) { if (modified.includes('failed') || modified.includes('problem')) { modified += " But we'll get it next time!"; } } return modified; } // Agent mood system const AGENT_MOODS = ['energized', 'neutral', 'tired', 'frustrated', 'proud', 'anxious']; function updateAgentMood(agent, event) { if (!agent.personality) return; const prevMood = agent.mood || 'neutral'; let newMood = prevMood; switch (event) { case 'success': newMood = agent.personality.optimism > 60 ? 'proud' : 'neutral'; agent.moodCounter = (agent.moodCounter || 0) + 1; break; case 'failure': agent.failureCount = (agent.failureCount || 0) + 1; if (agent.failureCount >= 3) { newMood = 'frustrated'; agent.failureCount = 0; } break; case 'level_up': newMood = 'energized'; break; case 'long_task': newMood = agent.personality.loyalty > 70 ? 'neutral' : 'tired'; break; case 'combat': newMood = agent.personality.boldness > 70 ? 'energized' : 'anxious'; break; } if (newMood !== prevMood) { agent.mood = newMood; agent.moodChangedAt = performance.now(); } } // Get mood modifier for success rate function getMoodModifier(agent) { if (!agent.mood) return 1.0; switch (agent.mood) { case 'energized': return 1.1; // +10% success case 'proud': return 1.05; // +5% success case 'tired': return 0.95; // -5% success case 'frustrated': return 0.9; // -10% success case 'anxious': return 0.97; // -3% success default: return 1.0; } } // Fleet state let agentFleet = []; const agentLookup = new Map(); // v8.18: O(1) agent lookup by ID instead of O(n) .find() let agentFleetPanelOpen = false; let agentUpdateTimers = {}; // Toggle fleet panel function toggleAgentFleetPanel() { agentFleetPanelOpen = !agentFleetPanelOpen; const panel = document.getElementById('agent-fleet-panel'); panel.classList.toggle('active', agentFleetPanelOpen); // v5.14: Refresh profile dropdown when opening if (agentFleetPanelOpen) { refreshProfileSelects(); } } // v7.22: Expose to window for inline onclick handler window.toggleAgentFleetPanel = toggleAgentFleetPanel; // v5.14: Wrapper to spawn agent with selected profile function spawnAgentWithProfile(agentType) { const profileSelect = document.getElementById('agent-spawn-profile'); const profileId = profileSelect?.value || ''; spawnAgent(agentType, profileId ? { profileId } : null); } // v5.12.1: Spawn a new agent with optional custom transcript and endpoint config // customConfig: { transcript: [...], endpoint: {...}, name: 'Custom Name', profileId: 'profile_xxx' } function spawnAgent(agentType, customConfig = null) { // v9.10: Block agent spawning in customOnly worlds if (window.WORLD_SYSTEMS?.customOnly === true) { console.log('[WORLD] Agent spawn blocked: customOnly world'); return null; } if (window.WORLD_SYSTEMS?.agents === false) { console.log('[WORLD] Agent spawn blocked: agents system disabled'); return null; } if (agentFleet.length >= MAX_AGENTS) { addCopilotMessage(`Fleet capacity reached (${MAX_AGENTS} agents). Recall an agent first.`, 'ai'); return null; } const typeConfig = AGENT_TYPES[agentType]; if (!typeConfig) return null; // Find next available name const usedNames = agentFleet.map(a => a.name); const availableName = customConfig?.name || AGENT_NAMES.find(n => !usedNames.includes(n)) || `Agent-${agentFleet.length + 1}`; // Merge custom transcript with base if provided let transcript = [...typeConfig.baseTranscript]; if (customConfig?.transcript) { transcript = customConfig.transcript; } // Extract endpoint config from transcript if present let endpointConfig = customConfig?.endpoint || null; if (!endpointConfig && transcript.length > 0) { // Check if first message contains endpoint config const systemMsg = transcript[0]; if (systemMsg.endpoint) { endpointConfig = systemMsg.endpoint; } } // v6.3.0: Agent now spawns immediately ready to work const agent = { id: Date.now().toString(36) + Math.random().toString(36).substr(2, 9), name: availableName, type: agentType, typeConfig: typeConfig, status: 'working', // v6.3.0: Start as working, not initializing statusMessage: 'Systems online!', // v6.3.0: Immediate feedback progress: 0, conversationHistory: transcript, results: [], lastDecisionTime: 0, mesh: null, // v6.4.1: Better initial position - use player, ship, or random (never origin) position: worldState.player ? worldState.player.position.clone() : (SHIP_STATE?.position ? SHIP_STATE.position.clone() : new THREE.Vector3((Math.random() - 0.5) * 20, 0, (Math.random() - 0.5) * 20)), targetPosition: null, totalEarnings: { xp: 0, gold: 0, items: [] }, spawnTime: performance.now(), // v5.12.1: Endpoint configuration endpointConfig: endpointConfig, activeEndpoint: null, // v5.14: Assigned endpoint profile profileId: customConfig?.profileId || null, // v5.15.2: Try Again replay system - store full interaction history interactionHistory: [], // Full request/response pairs with context replayState: null, // Current replay comparison if active // v5.17: Agent experience and efficiency system agentXP: 0, agentLevel: 1, efficiency: 1.0, // Multiplier for action success rate combo: 0, // Consecutive successful actions maxCombo: 0, // Best combo achieved lastActionSuccess: false, actionsPerformed: 0, successfulActions: 0, lastHealthRegen: performance.now(), // v6.3.0: Initialize taskState immediately (was in createAgentMesh) // v6.4.0: Added inventory system for resource hauling taskState: { currentTask: null, targetObject: null, targetPosition: null, state: 'working', // idle, moving, working, combat, returning, depositing, stuck, alert stuckCounter: 0, lastPosition: null, lastTaskTime: 0, alert: null, actionCooldown: 0, hp: 50, maxHp: 50, taskLog: [], // v6.4.0: Agent inventory for hauling resources inventory: [], // Items the agent is carrying carryingCapacity: 6, // Max items agent can carry (upgrades with level) totalHauled: 0, // Lifetime resources delivered to ship tripsCompleted: 0, // Number of round trips to ship // v7.86: Pre-allocated vector for target setting to avoid clone() allocations _targetVec: new THREE.Vector3() }, meshPending: false // Will be set true if mesh creation fails }; // Create 3D mesh for the agent (may fail if scene not ready) createAgentMesh(agent); // Add to fleet agentFleet.push(agent); agentLookup.set(agent.id, agent); // v8.18: Add to lookup Map for O(1) access // Update UI updateFleetUI(); updateFleetButton(); // Start autonomous loop for this agent startAgentLoop(agent); // Announce addCopilotMessage(`${typeConfig.icon} ${availableName} (${typeConfig.name}) deployed! They'll work autonomously and report back.`, 'ai'); return agent; } // v5.16: Create distinct 3D mesh for each agent type (mini-robots) // v6.3.0: Now retries if scene isn't ready yet function createAgentMesh(agent) { if (!scene) { // v6.3.0: Scene not ready - schedule retry agent.meshPending = true; // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`Agent ${agent.name}: Scene not ready, will retry mesh creation`); return; } agent.meshPending = false; const color = agent.typeConfig.color; const agentGroup = new THREE.Group(); // Create mini-robot body based on agent type const bodyHeight = 0.8; const bodyWidth = 0.5; // Body (cylinder with rounded appearance) const bodyGeom = new THREE.CylinderGeometry(bodyWidth * 0.4, bodyWidth * 0.5, bodyHeight, 8); const bodyMat = new THREE.MeshStandardMaterial({ color: 0x334455, metalness: 0.7, roughness: 0.3 }); const body = new THREE.Mesh(bodyGeom, bodyMat); body.position.y = bodyHeight / 2; body.castShadow = true; agentGroup.add(body); // Head (sphere with type-specific color visor) const headGeom = new THREE.SphereGeometry(0.25, 12, 12); const headMat = new THREE.MeshStandardMaterial({ color: 0x445566, metalness: 0.6, roughness: 0.4 }); const head = new THREE.Mesh(headGeom, headMat); head.position.y = bodyHeight + 0.2; agentGroup.add(head); // Visor/Eye (type-colored, glowing) const visorGeom = new THREE.BoxGeometry(0.3, 0.08, 0.15); const visorMat = new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.8, metalness: 0.9, roughness: 0.1 }); const visor = new THREE.Mesh(visorGeom, visorMat); visor.position.set(0, bodyHeight + 0.2, 0.2); agentGroup.add(visor); agent.visor = visor; // Type-specific tool/accessory const toolGroup = new THREE.Group(); switch (agent.type) { case 'gatherer': // Pickaxe const pickHandle = new THREE.Mesh( new THREE.CylinderGeometry(0.03, 0.03, 0.4, 6), new THREE.MeshStandardMaterial({ color: 0x8B4513 }) ); pickHandle.rotation.z = Math.PI / 4; const pickHead = new THREE.Mesh( new THREE.BoxGeometry(0.2, 0.08, 0.05), new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.8 }) ); pickHead.position.y = 0.2; pickHandle.add(pickHead); toolGroup.add(pickHandle); break; case 'hunter': // Sword const swordBlade = new THREE.Mesh( new THREE.BoxGeometry(0.05, 0.5, 0.02), new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.9 }) ); const swordHilt = new THREE.Mesh( new THREE.BoxGeometry(0.15, 0.08, 0.03), new THREE.MeshStandardMaterial({ color: 0x8B4513 }) ); swordHilt.position.y = -0.25; swordBlade.add(swordHilt); toolGroup.add(swordBlade); break; case 'miner': // Mining drill const drill = new THREE.Mesh( new THREE.ConeGeometry(0.1, 0.4, 8), new THREE.MeshStandardMaterial({ color: 0x666666, metalness: 0.8 }) ); drill.rotation.z = -Math.PI / 2; toolGroup.add(drill); break; case 'healer': // Medical cross const crossH = new THREE.Mesh( new THREE.BoxGeometry(0.25, 0.08, 0.04), new THREE.MeshStandardMaterial({ color: 0xff4444, emissive: 0xff0000, emissiveIntensity: 0.3 }) ); const crossV = new THREE.Mesh( new THREE.BoxGeometry(0.08, 0.25, 0.04), new THREE.MeshStandardMaterial({ color: 0xff4444, emissive: 0xff0000, emissiveIntensity: 0.3 }) ); toolGroup.add(crossH, crossV); break; case 'scout': case 'explorer': // Antenna const antenna = new THREE.Mesh( new THREE.CylinderGeometry(0.02, 0.02, 0.3, 6), new THREE.MeshStandardMaterial({ color: 0x888888 }) ); const antennaTip = new THREE.Mesh( new THREE.SphereGeometry(0.05, 8, 8), new THREE.MeshStandardMaterial({ color: color, emissive: color, emissiveIntensity: 0.5 }) ); antennaTip.position.y = 0.15; antenna.add(antennaTip); antenna.position.y = bodyHeight + 0.45; agentGroup.add(antenna); agent.antenna = antennaTip; break; case 'protector': // Shield const shield = new THREE.Mesh( new THREE.BoxGeometry(0.05, 0.35, 0.25), new THREE.MeshStandardMaterial({ color: 0x4488ff, metalness: 0.7 }) ); toolGroup.add(shield); break; case 'fisher': // Fishing rod const rod = new THREE.Mesh( new THREE.CylinderGeometry(0.02, 0.015, 0.6, 6), new THREE.MeshStandardMaterial({ color: 0x8B4513 }) ); rod.rotation.z = Math.PI / 6; toolGroup.add(rod); break; } toolGroup.position.set(0.35, bodyHeight * 0.6, 0); agentGroup.add(toolGroup); agent.tool = toolGroup; // Legs (simple cylinders) [-0.12, 0.12].forEach(xOff => { const leg = new THREE.Mesh( new THREE.CylinderGeometry(0.06, 0.08, 0.3, 6), new THREE.MeshStandardMaterial({ color: 0x333344, metalness: 0.5 }) ); leg.position.set(xOff, 0.15, 0); agentGroup.add(leg); }); // Alert indicator (hidden by default) const alertGeom = new THREE.SphereGeometry(0.15, 8, 8); const alertMat = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0 }); const alertIndicator = new THREE.Mesh(alertGeom, alertMat); alertIndicator.position.y = bodyHeight + 0.6; agentGroup.add(alertIndicator); agent.alertIndicator = alertIndicator; // Glow effect (type color) const glowGeom = new THREE.SphereGeometry(0.7, 8, 8); const glowMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.1 }); const glow = new THREE.Mesh(glowGeom, glowMat); glow.position.y = bodyHeight / 2; agentGroup.add(glow); agent.glow = glow; // v6.5.2: MUCH larger scale so agents are clearly visible on the map agentGroup.scale.setScalar(4.0); // v6.4.1: Position agents with proper spread - NEVER at origin // Use agent index in fleet for deterministic spread const agentIndex = agentFleet.indexOf(agent); const spreadAngle = (agentIndex * 0.7) + Math.random() * 0.5; // Unique angle per agent const spreadDist = 5 + Math.random() * 8; // 5-13 units from center let baseX = 0, baseZ = 0; // Get base position from player, ship, or default if (worldState.player && worldState.player.position) { baseX = worldState.player.position.x; baseZ = worldState.player.position.z; } else if (SHIP_STATE && SHIP_STATE.position) { baseX = SHIP_STATE.position.x; baseZ = SHIP_STATE.position.z; } else { // Default to center-ish of map with spread baseX = (Math.random() - 0.5) * 20; baseZ = (Math.random() - 0.5) * 20; } // Apply spread offset based on agent index const finalX = baseX + Math.cos(spreadAngle) * spreadDist; const finalZ = baseZ + Math.sin(spreadAngle) * spreadDist; // v6.5.1: Get terrain height BEFORE positioning - agents were spawning underground! let terrainY = 0; if (typeof getTerrainHeight === 'function') { terrainY = getTerrainHeight(finalX, finalZ); } agentGroup.position.set(finalX, terrainY, finalZ); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`Agent ${agent.name}: Spawned at (${finalX.toFixed(1)}, ${terrainY.toFixed(1)}, ${finalZ.toFixed(1)}) - index ${agentIndex}`); agent.mesh = agentGroup; // v8.23: Create position Vector3 once, then use copy() for updates if (!agent.position) { agent.position = new THREE.Vector3(); } agent.position.copy(agentGroup.position); // v6.3.0: taskState is now initialized in spawnAgent - just update position reference // v8.23: Use copy() instead of clone() to avoid allocation if (agent.taskState) { if (!agent.taskState.lastPosition) { agent.taskState.lastPosition = new THREE.Vector3(); } agent.taskState.lastPosition.copy(agent.position); } scene.add(agentGroup); // v6.5.1: Ensure snapped to ground after scene add if (typeof snapToGround === 'function') { snapToGround(agentGroup); } // v6.4.1: Visual spawn effect if (particles) { particles.emit(agentGroup.position, 8, agent.typeConfig.color, { spread: 1.5, lifetime: 600, size: 0.12 }); } } // Start autonomous decision loop for an agent // v6.60: DETERMINISTIC PRIMARY - API calls are supplementary hints only // Core decisions based on map state, endpoint enhances but never replaces function startAgentLoop(agent) { // IMMEDIATELY set agent to working status agent.status = 'working'; agent.statusMessage = 'Systems online!'; updateAgentCardUI(agent); // Run first deterministic action IMMEDIATELY based on map state runDeterministicAgentCommand(agent); agent.lastDecisionTime = performance.now(); agent.lastApiCallTime = 0; // Track API calls separately agent.apiHint = null; // Store hints from API // Mesh retry counter let meshRetryCount = 0; const MAX_MESH_RETRIES = 50; // Try for ~25 seconds const loop = () => { if (!agent || !agentFleet.includes(agent)) return; // v6.4.1: ROBUST mesh creation retry - createAgentMesh now handles positioning if (!agent.mesh && scene && meshRetryCount < MAX_MESH_RETRIES) { meshRetryCount++; // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`Agent ${agent.name}: Attempting mesh creation (attempt ${meshRetryCount})`); createAgentMesh(agent); if (agent.mesh) { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`Agent ${agent.name}: Mesh created successfully at (${agent.mesh.position.x.toFixed(1)}, ${agent.mesh.position.z.toFixed(1)})`); updateAgentCardUI(agent); } } const now = performance.now(); // v6.60: PRIMARY - Run deterministic commands based on map state if (now - agent.lastDecisionTime >= agent.typeConfig.decisionInterval) { agent.lastDecisionTime = now; try { // CORE: Deterministic command based on agent type and map state runDeterministicAgentCommand(agent); } catch (err) { console.error(`Agent ${agent.name} deterministic error:`, err); } } // v6.60: SUPPLEMENTARY - API calls for hints (every 15 seconds, non-blocking) const API_HINT_INTERVAL = 15000; // 15 seconds if (now - agent.lastApiCallTime >= API_HINT_INTERVAL) { agent.lastApiCallTime = now; // Non-blocking API hint request (doesn't affect core behavior) requestAgentApiHint(agent).catch(() => {}); } // Continue loop agentUpdateTimers[agent.id] = setTimeout(loop, 500); }; // Start loop immediately loop(); } // v6.60: CORE DETERMINISTIC COMMAND - Based on agent type and actual map state // This is the PRIMARY decision-making logic - runs without any API function runDeterministicAgentCommand(agent) { if (!agent.mesh) return; const task = agent.taskState; const agentPos = agent.mesh.position; const taskType = agent.typeConfig.taskType; // Build map context for decision const mapContext = scanMapContextForAgent(agent); // Deterministic command based on agent type and map state switch (taskType) { case 'gather': executeGathererCommand(agent, mapContext); break; case 'hunt': executeHunterCommand(agent, mapContext); break; case 'scout': executeScoutCommand(agent, mapContext); break; case 'protect': executeProtectorCommand(agent, mapContext); break; case 'heal': executeHealerCommand(agent, mapContext); break; case 'fish': executeFisherCommand(agent, mapContext); break; case 'mine': executeMinerCommand(agent, mapContext); break; case 'terraform': executeTerraformerCommand(agent, mapContext); break; case 'build': executeBuilderCommand(agent, mapContext); break; default: executeGathererCommand(agent, mapContext); } // Update progress const elapsed = performance.now() - agent.spawnTime; agent.progress = Math.min(100, (elapsed / 60000) * 100); saveGameData(); updateAgentCardUI(agent); } // v6.60: Scan actual map state for deterministic decisions // v7.74: Use distanceToSquared for performance - sqrt only when needed for output function scanMapContextForAgent(agent) { if (!agent.mesh) return {}; const agentPos = agent.mesh.position; const scanRadius = 40; // World units to scan const scanRadiusSq = scanRadius * scanRadius; // v7.74: Squared for comparison const context = { nearbyResources: [], nearbyEnemies: [], nearbyAllies: [], nearbyFishingSpots: [], nearbyConstructionSites: [], terrainData: [], playerPosition: worldState.player?.position?.clone() || null, playerHealth: gameData.player?.hp || 100, playerMaxHealth: gameData.player?.maxHp || 100, agentInventory: agent.taskState?.inventory || [], agentCarryingCapacity: agent.taskState?.carryingCapacity || 6, apiHint: agent.apiHint // Include any API-provided hints }; // Scan interactables (trees, rocks, plants) // v8.09: forEach to for loop + InteractableSpatialGrid for O(1) nearby lookup if (worldState.interactables) { // Rebuild grid if needed (shared across all agents per frame) // checkRebuild auto-detects array length changes (additions/removals) if (InteractableSpatialGrid.checkRebuild(worldState.interactables)) { InteractableSpatialGrid.rebuild(worldState.interactables); } // Use spatial grid for O(1) lookup instead of O(n) full iteration const scanCellRadius = Math.ceil(scanRadius / InteractableSpatialGrid.cellSize); const nearbyInteractables = InteractableSpatialGrid.getNearby(agentPos.x, agentPos.z, scanCellRadius); for (let i = 0, len = nearbyInteractables.length; i < len; i++) { const obj = nearbyInteractables[i]; if (!obj.parent) continue; const distSq = agentPos.distanceToSquared(obj.position); if (distSq <= scanRadiusSq) { context.nearbyResources.push({ object: obj, position: obj.position.clone(), distance: Math.sqrt(distSq), // Only sqrt for output type: obj.userData?.type || 'unknown', name: obj.userData?.name || 'Resource', hp: obj.userData?.hp || 0 }); } } // Sort by distance context.nearbyResources.sort((a, b) => a.distance - b.distance); } // Scan enemies // v8.03: Converted forEach to for loop for performance if (worldState.mobs) { const mobs = worldState.mobs; for (let i = 0, len = mobs.length; i < len; i++) { const mob = mobs[i]; if (!mob.mesh || mob.isDead) continue; const distSq = agentPos.distanceToSquared(mob.mesh.position); if (distSq <= scanRadiusSq) { context.nearbyEnemies.push({ mob: mob, position: mob.mesh.position.clone(), distance: Math.sqrt(distSq), hp: mob.hp || 0, type: mob.type || 'enemy', isBoss: mob.isBoss || false }); } } context.nearbyEnemies.sort((a, b) => a.distance - b.distance); } // Scan fishing spots // v8.03: Converted forEach to for loop for performance if (worldState.fishingSpots) { const spots = worldState.fishingSpots; for (let i = 0, len = spots.length; i < len; i++) { const spot = spots[i]; if (!spot.parent) continue; const distSq = agentPos.distanceToSquared(spot.position); if (distSq <= scanRadiusSq) { context.nearbyFishingSpots.push({ spot: spot, position: spot.position.clone(), distance: Math.sqrt(distSq) }); } } context.nearbyFishingSpots.sort((a, b) => a.distance - b.distance); } // Scan construction sites // v8.03: Converted forEach to for loop for performance if (worldState.constructionSites) { const sites = worldState.constructionSites; for (let i = 0, len = sites.length; i < len; i++) { const site = sites[i]; if (!site.mesh) continue; const distSq = agentPos.distanceToSquared(site.mesh.position); if (distSq <= scanRadiusSq) { context.nearbyConstructionSites.push({ site: site, position: site.mesh.position.clone(), distance: Math.sqrt(distSq), claimed: site.claimedBy || null }); } } context.nearbyConstructionSites.sort((a, b) => a.distance - b.distance); } // Scan other agents (allies) // v8.03: Converted forEach to for loop for performance for (let i = 0, len = agentFleet.length; i < len; i++) { const otherAgent = agentFleet[i]; if (otherAgent.id === agent.id || !otherAgent.mesh) continue; const distSq = agentPos.distanceToSquared(otherAgent.mesh.position); if (distSq <= scanRadiusSq) { context.nearbyAllies.push({ agent: otherAgent, position: otherAgent.mesh.position.clone(), distance: Math.sqrt(distSq), type: otherAgent.type, health: otherAgent.taskState?.hp || 50 }); } } return context; } // v6.60: Deterministic Gatherer Command function executeGathererCommand(agent, mapContext) { const task = agent.taskState; // Initialize inventory if needed if (!task.inventory) task.inventory = []; if (!task.carryingCapacity) task.carryingCapacity = 6 + Math.floor(agent.agentLevel / 2); task.carryingCapacity = 6 + Math.floor(agent.agentLevel / 2); // COMMAND: If inventory full, return to ship if (task.inventory.length >= task.carryingCapacity) { if (task.state !== 'returning' && task.state !== 'depositing') { task.state = 'returning'; // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, SHIP_STATE.position); agent.statusMessage = `📦 Inventory full (${task.inventory.length}/${task.carryingCapacity}) - returning to ship`; logAgentTask(agent, 'Inventory full, heading to ship'); } return; } // COMMAND: Find nearest gatherable resource (trees, plants, bushes) const gatherTargets = mapContext.nearbyResources.filter(r => r.type === 'tree' || r.name?.includes('Tree') || r.name?.includes('Bush') || r.name?.includes('Plant') || r.name?.includes('Herb') || r.type === 'plant' ); if (gatherTargets.length > 0) { // API hint might suggest a specific resource type let target = gatherTargets[0]; if (agent.apiHint?.preferredResource) { const hintTarget = gatherTargets.find(t => t.name?.toLowerCase().includes(agent.apiHint.preferredResource.toLowerCase()) ); if (hintTarget) target = hintTarget; } task.targetObject = target.object; task.targetPosition = target.position; task.state = 'moving'; agent.statusMessage = `🎯 Targeting ${target.name} (${target.distance.toFixed(0)}m)`; // If close enough, harvest if (target.distance < 2) { performAgentHarvest(agent, target.object); trackAgentAction(agent, true, 5); } } else { // No resources found - wander to explore // v7.74: Use distanceToSquared for performance if (!task.targetPosition || agent.mesh.position.distanceToSquared(task.targetPosition) < 4) { // 2*2=4 task.targetPosition = getRandomWanderPosition(agent.mesh.position); task.state = 'moving'; agent.statusMessage = '🔎 Searching for resources...'; } } } // v6.60: Deterministic Hunter Command function executeHunterCommand(agent, mapContext) { const task = agent.taskState; // Filter out bosses unless we're high level const huntTargets = mapContext.nearbyEnemies.filter(e => !e.isBoss || agent.agentLevel >= 5 ); if (huntTargets.length > 0) { const target = huntTargets[0]; // If close enough, attack if (target.distance < 3) { const damage = 10 + agent.agentLevel * 2; target.mob.hp -= damage; if (agent.mesh) { spawnFloater(target.position, `-${damage}`, '#ff4444'); spawnAgentParticleEffect(agent, 'success'); } agent.statusMessage = `⚔️ Attacking! -${damage} damage`; // Check if killed if (target.mob.hp <= 0) { target.mob.isDead = true; const xp = 15 + agent.agentLevel * 3; const gold = 5 + agent.agentLevel; if (typeof addXp === 'function') addXp('combat', xp); gameData.gold = (gameData.gold || 0) + gold; agent.totalEarnings.xp += xp; agent.totalEarnings.gold += gold; agent.statusMessage = `☠️ Enemy defeated! +${xp} XP +${gold} gold`; logAgentTask(agent, `Killed enemy: +${xp} XP +${gold} gold`); } trackAgentAction(agent, true, 8); task.actionCooldown = 800; // Attack cooldown } else { // Move to engage task.targetPosition = target.position; task.state = 'moving'; agent.statusMessage = `🏹 Engaging enemy (${target.distance.toFixed(0)}m)`; } } else { // Patrol - wander looking for enemies // v7.74: Use distanceToSquared for performance if (!task.targetPosition || agent.mesh.position.distanceToSquared(task.targetPosition) < 4) { // 2*2=4 task.targetPosition = getRandomWanderPosition(agent.mesh.position); task.state = 'moving'; agent.statusMessage = '🔍 Patrolling for hostiles...'; } } } // v6.60: Deterministic Scout Command function executeScoutCommand(agent, mapContext) { const task = agent.taskState; // Scouts report discoveries and explore const hasEnemies = mapContext.nearbyEnemies.length > 0; const hasResources = mapContext.nearbyResources.length > 0; // Report significant findings if (hasEnemies && !task.lastReportedEnemies) { const enemyCount = mapContext.nearbyEnemies.length; agent.statusMessage = `⚠️ Spotted ${enemyCount} hostile(s)!`; if (typeof addCopilotMessage === 'function') { addCopilotMessage(`🔍 ${agent.name} spotted ${enemyCount} enemies at (${Math.floor(agent.mesh.position.x)}, ${Math.floor(agent.mesh.position.z)})`, 'ai'); } task.lastReportedEnemies = true; agent.results.push({ discovery: `Spotted ${enemyCount} enemies` }); trackAgentAction(agent, true, 6); } else if (!hasEnemies) { task.lastReportedEnemies = false; } // Always explore - scouts move faster and further // v7.74: Use distanceToSquared for performance if (!task.targetPosition || agent.mesh.position.distanceToSquared(task.targetPosition) < 9) { // 3*3=9 const angle = Math.random() * Math.PI * 2; const distance = 15 + Math.random() * 20; // Scouts go further const halfWorld = (CONFIG.WORLD_SIZE / 2) * CONFIG.TILE_SIZE; task.targetPosition = new THREE.Vector3( Math.max(-halfWorld + 5, Math.min(halfWorld - 5, agent.mesh.position.x + Math.cos(angle) * distance)), agent.mesh.position.y, Math.max(-halfWorld + 5, Math.min(halfWorld - 5, agent.mesh.position.z + Math.sin(angle) * distance)) ); task.state = 'moving'; agent.statusMessage = '🧭 Scouting new territory...'; } } // v6.60: Deterministic Protector Command function executeProtectorCommand(agent, mapContext) { const task = agent.taskState; // Priority 1: Engage nearby enemies if (mapContext.nearbyEnemies.length > 0) { const target = mapContext.nearbyEnemies[0]; if (target.distance < 4) { const damage = 12 + agent.agentLevel * 2; target.mob.hp -= damage; if (agent.mesh) { spawnFloater(target.position, `-${damage}`, '#ff4444'); spawnAgentParticleEffect(agent, 'success'); } agent.statusMessage = `🛡️ Defending! -${damage} damage`; trackAgentAction(agent, true, 6); task.actionCooldown = 600; if (target.mob.hp <= 0) { target.mob.isDead = true; agent.statusMessage = '🛡️ Threat neutralized!'; } } else { task.targetPosition = target.position; task.state = 'moving'; agent.statusMessage = `🛡️ Intercepting threat (${target.distance.toFixed(0)}m)`; } } else { // No enemies - patrol near player // v7.74: Use distanceToSquared for performance if (mapContext.playerPosition) { const distSqToPlayer = agent.mesh.position.distanceToSquared(mapContext.playerPosition); if (distSqToPlayer > 225) { // 15*15=225 // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, mapContext.playerPosition); task.state = 'moving'; agent.statusMessage = '🛡️ Returning to player...'; } else { agent.statusMessage = '🛡️ Area secure. Guarding player.'; task.state = 'idle'; } } } } // v6.60: Deterministic Healer Command // v7.74: Use distanceToSquared for performance function executeHealerCommand(agent, mapContext) { const task = agent.taskState; // Priority 1: Heal player if injured if (mapContext.playerHealth < mapContext.playerMaxHealth * 0.8) { if (mapContext.playerPosition) { const distSqToPlayer = agent.mesh.position.distanceToSquared(mapContext.playerPosition); if (distSqToPlayer < 25) { // 5*5=25 const healAmount = 8 + agent.agentLevel * 2; gameData.player.hp = Math.min(gameData.player.maxHp, gameData.player.hp + healAmount); updateHealthUI(); agent.statusMessage = `💚 Healed player +${healAmount} HP`; agent.results.push({ heal: healAmount }); if (agent.mesh) spawnAgentParticleEffect(agent, 'heal'); trackAgentAction(agent, true, 5); task.actionCooldown = 2000; // Heal cooldown } else { task.targetPosition = mapContext.playerPosition; task.state = 'moving'; const distToPlayer = Math.sqrt(distSqToPlayer); // Only sqrt for display agent.statusMessage = `💚 Moving to heal player (${distToPlayer.toFixed(0)}m)`; } } } else { // Priority 2: Heal injured allies const injuredAlly = mapContext.nearbyAllies.find(a => a.health < 40); if (injuredAlly) { if (injuredAlly.distance < 4) { injuredAlly.agent.taskState.hp = Math.min(50, (injuredAlly.agent.taskState.hp || 50) + 10); agent.statusMessage = `💚 Healed ${injuredAlly.agent.name}`; trackAgentAction(agent, true, 4); } else { task.targetPosition = injuredAlly.position; task.state = 'moving'; } } else { // Stay near player if (mapContext.playerPosition) { const distSqToPlayer2 = agent.mesh.position.distanceToSquared(mapContext.playerPosition); if (distSqToPlayer2 > 100) { // 10*10=100 task.targetPosition = mapContext.playerPosition; task.state = 'moving'; } } agent.statusMessage = '💚 All healthy. Standing by.'; } } } // v6.60: Deterministic Fisher Command function executeFisherCommand(agent, mapContext) { const task = agent.taskState; // Initialize inventory if (!task.inventory) task.inventory = []; if (!task.carryingCapacity) task.carryingCapacity = 8 + Math.floor(agent.agentLevel / 2); // Return to ship if full if (task.inventory.length >= task.carryingCapacity) { if (task.state !== 'returning' && task.state !== 'depositing') { task.state = 'returning'; // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, SHIP_STATE.position); agent.statusMessage = `🐟 Haul full! Returning (${task.inventory.length} fish)`; } return; } // Find fishing spots if (mapContext.nearbyFishingSpots.length > 0) { const spot = mapContext.nearbyFishingSpots[0]; if (spot.distance < 3) { // Fish! task.state = 'working'; // Deterministic catch based on level const catchChance = 0.3 + agent.agentLevel * 0.05; if (Math.random() < catchChance) { const fishTypes = ['Raw Fish']; if (agent.agentLevel >= 3) fishTypes.push('Large Fish'); if (agent.agentLevel >= 5) fishTypes.push('Golden Fish'); const fishType = fishTypes[Math.floor(Math.random() * fishTypes.length)]; task.inventory.push(fishType); agent.totalEarnings.items.push({ item: fishType, amount: 1 }); agent.statusMessage = fishType === 'Golden Fish' ? `🌟 Caught a Golden Fish! [${task.inventory.length}]` : `🐟 Caught ${fishType}! [${task.inventory.length}/${task.carryingCapacity}]`; spawnFloater(agent.mesh.position, `+1 ${fishType}`, '#00ffff'); if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); trackAgentAction(agent, true, 4); } else { agent.statusMessage = '🎣 Waiting for a bite...'; } task.actionCooldown = 1500; } else { task.targetPosition = spot.position; task.state = 'moving'; agent.statusMessage = `🎣 Heading to fishing spot (${spot.distance.toFixed(0)}m)`; } } else { // Search for water/fishing spots // v7.74: Use distanceToSquared for performance if (!task.targetPosition || agent.mesh.position.distanceToSquared(task.targetPosition) < 4) { // 2*2=4 task.targetPosition = getRandomWanderPosition(agent.mesh.position); task.state = 'moving'; agent.statusMessage = '🔍 Searching for fishing spots...'; } } } // v6.60: Deterministic Miner Command function executeMinerCommand(agent, mapContext) { const task = agent.taskState; // Initialize inventory if (!task.inventory) task.inventory = []; if (!task.carryingCapacity) task.carryingCapacity = 8 + Math.floor(agent.agentLevel / 2); task.carryingCapacity = 8 + Math.floor(agent.agentLevel / 2); // Return if full if (task.inventory.length >= task.carryingCapacity) { if (task.state !== 'returning' && task.state !== 'depositing') { task.state = 'returning'; // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, SHIP_STATE.position); agent.statusMessage = `⛏️ Inventory full - returning`; } return; } // Find mining targets const mineTargets = mapContext.nearbyResources.filter(r => r.type === 'rock' || r.name?.includes('Ore') || r.name?.includes('Crystal') || r.name?.includes('Stone') ); if (mineTargets.length > 0) { const target = mineTargets[0]; if (target.distance < 2) { performAgentHarvest(agent, target.object); trackAgentAction(agent, true, 6); } else { task.targetObject = target.object; task.targetPosition = target.position; task.state = 'moving'; agent.statusMessage = `⛏️ Mining ${target.name} (${target.distance.toFixed(0)}m)`; } } else { // Search for deposits // v7.74: Use distanceToSquared for performance if (!task.targetPosition || agent.mesh.position.distanceToSquared(task.targetPosition) < 4) { // 2*2=4 task.targetPosition = getRandomWanderPosition(agent.mesh.position); task.state = 'moving'; agent.statusMessage = '⛏️ Searching for ore deposits...'; } } } // v6.60: Deterministic Terraformer Command (uses existing intelligent logic) function executeTerraformerCommand(agent, mapContext) { // Terraformer already has intelligent site selection in simulateAgentDecision // Call the existing terraform case logic const gameContext = buildGameContextForAgent(agent); // Use the existing terraform logic (already deterministic) const taskType = 'terraform'; const rand = Math.random(); const terraformRate = getAgentSuccessRate(agent, 0.7); if (rand < terraformRate && agent.mesh) { // Reuse existing intelligent site scanner from simulateAgentDecision simulateTerraformAction(agent); } else { agent.statusMessage = '📡 Scanning terrain topology...'; } trackAgentAction(agent, true, 10); } // v6.60: Helper for terraform (extracted from simulateAgentDecision) function simulateTerraformAction(agent) { if (!agent.mesh) return; const agentX = Math.floor((agent.mesh.position.x / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); const agentZ = Math.floor((agent.mesh.position.z / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); // Calculate terrain roughness in an area const calculateRoughness = (cx, cz, radius) => { let heights = []; for (let dx = -radius; dx <= radius; dx++) { for (let dz = -radius; dz <= radius; dz++) { const tx = cx + dx, tz = cz + dz; if (worldState.terrain[tx] && worldState.terrain[tx][tz] !== undefined && worldState.terrain[tx][tz] > 0) { heights.push(worldState.terrain[tx][tz]); } } } if (heights.length < 2) return 0; const avg = heights.reduce((a, b) => a + b, 0) / heights.length; const variance = heights.reduce((sum, h) => sum + Math.pow(h - avg, 2), 0) / heights.length; return Math.sqrt(variance); }; const roughness = calculateRoughness(agentX, agentZ, 2); if (roughness > 0.2) { // Smooth the terrain const smoothRadius = 2; let totalHeight = 0, count = 0; let heightMap = []; for (let dx = -smoothRadius; dx <= smoothRadius; dx++) { for (let dz = -smoothRadius; dz <= smoothRadius; dz++) { const tx = agentX + dx, tz = agentZ + dz; if (worldState.terrain[tx] && worldState.terrain[tx][tz] !== undefined && worldState.terrain[tx][tz] > 0) { const h = worldState.terrain[tx][tz]; totalHeight += h; count++; heightMap.push({ tx, tz, h }); } } } if (count > 0) { const avgHeight = totalHeight / count; for (const cell of heightMap) { worldState.terrain[cell.tx][cell.tz] = cell.h + (avgHeight - cell.h) * 0.95; } // v9.4: Update the 3D terrain mesh visuals if (typeof worldState.updateTerrainMeshes === 'function') { worldState.updateTerrainMeshes(agentX, agentZ, smoothRadius + 1); } agent.statusMessage = `🚜 Smoothed terrain at (${agentX}, ${agentZ})`; if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); } } else { // Area smooth - move to find rough terrain moveAgentToRandomPosition(agent); agent.statusMessage = '🔍 Seeking uneven terrain...'; } } // v6.60: Deterministic Builder Command (uses existing intelligent logic) function executeBuilderCommand(agent, mapContext) { const task = agent.taskState; // Find unclaimed construction sites const availableSites = mapContext.nearbyConstructionSites.filter(s => !s.claimed || s.claimed === agent.name ); if (availableSites.length > 0) { const site = availableSites[0]; if (site.distance < 3) { // Build at site task.state = 'working'; agent.statusMessage = `🔧 Building at (${Math.floor(site.position.x)}, ${Math.floor(site.position.z)})`; // Mark as claimed if (site.site) site.site.claimedBy = agent.name; // Building progress if (!site.site.buildProgress) site.site.buildProgress = 0; site.site.buildProgress += 10 + agent.agentLevel * 2; if (site.site.buildProgress >= 100) { // Complete construction agent.statusMessage = '🏗️ Construction complete!'; if (typeof addCopilotMessage === 'function') { addCopilotMessage(`🏗️ ${agent.name} completed construction at (${Math.floor(site.position.x)}, ${Math.floor(site.position.z)})!`, 'ai'); } // Remove from construction sites worldState.constructionSites = worldState.constructionSites.filter(s => s !== site.site); } if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); trackAgentAction(agent, true, 10); task.actionCooldown = 1000; } else { task.targetPosition = site.position; task.state = 'moving'; agent.statusMessage = `🔧 Heading to construction site (${site.distance.toFixed(0)}m)`; } } else { // Search for sites // v7.74: Use distanceToSquared for performance if (!task.targetPosition || agent.mesh.position.distanceToSquared(task.targetPosition) < 4) { // 2*2=4 task.targetPosition = getRandomWanderPosition(agent.mesh.position); task.state = 'moving'; agent.statusMessage = '🔍 Searching for construction sites...'; } } } // v6.60: SUPPLEMENTARY API hint request (non-blocking, enhances but doesn't replace) async function requestAgentApiHint(agent) { const endpoint = getAgentEndpoint(agent); // No endpoint = no hints (core logic handles everything) if (!endpoint || !endpoint.url || !endpoint.key) { return; } try { const gameContext = buildGameContextForAgent(agent); const mapSummary = summarizeMapForApi(agent); const hintRequest = { role: 'user', content: `Agent ${agent.name} (${agent.type}) needs a quick hint. Map: ${mapSummary}. Current task: ${agent.statusMessage}. Respond with brief JSON hint: {"preferredResource": "type or null", "priority": "gather|fight|explore|heal", "suggestion": "brief tip"}` }; const headers = { 'Content-Type': 'application/json' }; if (endpoint.headerPrefix) { headers[endpoint.headerStyle] = endpoint.headerPrefix + endpoint.key; } else { headers[endpoint.headerStyle] = endpoint.key; } const requestBody = formatAgentRequestBody(endpoint, hintRequest, [hintRequest], agent); const response = await fetch(endpoint.url, { method: 'POST', headers: headers, body: requestBody }); if (response.ok) { const data = await response.json(); const textResponse = parseAgentResponse(endpoint, data); // Try to parse hint try { const hint = JSON.parse(textResponse); agent.apiHint = hint; // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`Agent ${agent.name} received API hint:`, hint); } catch { // Non-JSON response - store as suggestion agent.apiHint = { suggestion: textResponse.substring(0, 100) }; } } } catch (err) { // Silently fail - API hints are supplementary console.debug(`Agent ${agent.name} API hint failed (non-critical):`, err.message); } } // v6.60: Summarize map state for API hint request // v7.80: distanceToSquared optimization function summarizeMapForApi(agent) { if (!agent.mesh) return 'No position data'; const pos = agent.mesh.position; const rangeSq = 400; // 20*20=400 const nearbyResources = worldState.interactables?.filter(o => o.parent && o.position.distanceToSquared(pos) < rangeSq ).length || 0; const nearbyEnemies = worldState.mobs?.filter(m => m.mesh && !m.isDead && m.mesh.position.distanceToSquared(pos) < rangeSq ).length || 0; return `pos:(${Math.floor(pos.x)},${Math.floor(pos.z)}) resources:${nearbyResources} enemies:${nearbyEnemies} inv:${agent.taskState?.inventory?.length || 0}`; } // v5.12.1: Make an autonomous decision for an agent via configurable API endpoint async function makeAgentDecision(agent) { // Get agent-specific endpoint (may differ from global) const endpoint = getAgentEndpoint(agent); // Build real-time context const gameContext = buildGameContextForAgent(agent); // Inject context into a user message const contextMessage = { role: 'user', content: `Current situation: ${JSON.stringify(gameContext)}. What's your next action?` }; // Build conversation for API const conversationForApi = [...agent.conversationHistory, contextMessage]; // If no endpoint configured, use simulated local decisions if (!endpoint || !endpoint.url || !endpoint.key) { // v5.15: Log why we're falling back to simulation // v8.26: Gated debug logging if (!endpoint) { if (DEBUG_LOGGING) console.log(`Agent ${agent.name}: No endpoint configured, using simulation`); } else if (!endpoint.url) { if (DEBUG_LOGGING) console.log(`Agent ${agent.name}: Missing endpoint URL, using simulation`); } else if (!endpoint.key) { if (DEBUG_LOGGING) console.log(`Agent ${agent.name}: Missing API key for ${endpoint.name}, using simulation. Check RAPPID settings.`); agent.statusMessage = 'API key missing - using simulation'; } simulateAgentDecision(agent, gameContext); return; } try { agent.status = 'thinking'; agent.activeEndpoint = endpoint.name; // Track which endpoint is being used updateAgentCardUI(agent); // Build headers based on endpoint configuration const headers = { 'Content-Type': 'application/json' }; // Add auth header based on endpoint style if (endpoint.headerPrefix) { headers[endpoint.headerStyle] = endpoint.headerPrefix + endpoint.key; } else { headers[endpoint.headerStyle] = endpoint.key; } // Format body based on endpoint type const requestBody = formatAgentRequestBody(endpoint, contextMessage, conversationForApi, agent); const response = await fetch(endpoint.url, { method: 'POST', headers: headers, body: requestBody }); if (response.ok) { const data = await response.json(); // Parse response based on endpoint type const textResponse = parseAgentResponse(endpoint, data); // v5.15.2: Store full interaction for Try Again replay const interactionRecord = { id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5), timestamp: Date.now(), contextMessage: { ...contextMessage }, gameContext: { ...gameContext }, endpoint: { name: endpoint.name, url: endpoint.url }, request: JSON.parse(requestBody), response: textResponse, rawResponse: data, conversationIndexBefore: agent.conversationHistory.length, executed: false // Will be set true after execution }; agent.interactionHistory.push(interactionRecord); // Keep last 20 interactions if (agent.interactionHistory.length > 20) { agent.interactionHistory.shift(); } // Add to conversation history (keep last 20 messages to avoid token overflow) agent.conversationHistory.push(contextMessage); agent.conversationHistory.push({ role: 'assistant', content: textResponse }); if (agent.conversationHistory.length > 22) { // Keep system message + last 20 agent.conversationHistory = [ agent.conversationHistory[0], ...agent.conversationHistory.slice(-20) ]; } // Parse and execute the decision parseAndExecuteAgentDecision(agent, textResponse); // v5.15.2: Mark interaction as executed interactionRecord.executed = true; interactionRecord.conversationIndexAfter = agent.conversationHistory.length; } else { // v5.15: Better error logging const errorText = await response.text().catch(() => ''); console.warn(`Agent ${agent.name} API error (${endpoint.name}): HTTP ${response.status}`, errorText.substring(0, 200)); if (response.status === 401 || response.status === 403) { agent.statusMessage = `Auth failed (${response.status}) - check API key`; } else { agent.statusMessage = `API error ${response.status} - using fallback`; } simulateAgentDecision(agent, gameContext); } } catch (error) { // v8.26: Enhanced error message with more context for debugging console.error(`[AGENT] v8.26: Decision error for "${agent.name}" using endpoint "${endpoint.name}". URL: ${endpoint.url?.substring(0, 50) || 'unknown'}. Error:`, error.message || error); agent.statusMessage = 'Network error - using fallback'; simulateAgentDecision(agent, gameContext); } agent.status = 'working'; updateAgentCardUI(agent); } // Build game context for agent decision function buildGameContextForAgent(agent) { const elapsed = (performance.now() - agent.spawnTime) / 1000; return { agent_name: agent.name, agent_type: agent.type, mission_time_seconds: Math.floor(elapsed), player_hp: gameData.player?.hp || 100, player_max_hp: gameData.player?.maxHp || 100, player_position: worldState.player ? { x: Math.floor(worldState.player.position.x), z: Math.floor(worldState.player.position.z) } : { x: 0, z: 0 }, current_biome: worldState?.currentCiv?.biomeName || 'Unknown', nearby_enemies: countNearbyEnemies(agent), items_gathered: agent.totalEarnings.items.length, xp_earned: agent.totalEarnings.xp, gold_earned: agent.totalEarnings.gold }; } // Count enemies near an agent // v7.80: distanceToSquared optimization function countNearbyEnemies(agent) { if (!worldState.mobs || !agent.position) return 0; const rangeSq = 225; // 15*15=225 return worldState.mobs.filter(mob => { if (!mob.mesh) return false; return mob.mesh.position.distanceToSquared(agent.position) < rangeSq; }).length; } // Parse API response and execute agent action function parseAndExecuteAgentDecision(agent, response) { try { // Try to extract JSON from response const jsonMatch = response.match(/\{[\s\S]*\}/); if (jsonMatch) { const decision = JSON.parse(jsonMatch[0]); executeAgentAction(agent, decision); } else { // No JSON, use response as status message agent.statusMessage = response.substring(0, 100); } } catch (e) { // Parse failed, treat as status update agent.statusMessage = response.substring(0, 100); } updateAgentCardUI(agent); } // Execute an agent action based on decision function executeAgentAction(agent, decision) { agent.statusMessage = decision.message || 'Working...'; // Process results if (decision.results) { decision.results.forEach(result => { if (result.item && result.amount) { addToInventory(result.item, result.amount); agent.totalEarnings.items.push({ item: result.item, amount: result.amount }); agent.results.push({ item: result.item, amount: result.amount }); } if (result.xp) { if (typeof addXp === 'function') addXp('combat', result.xp); agent.totalEarnings.xp += result.xp; } if (result.gold) { gameData.gold = (gameData.gold || 0) + result.gold; agent.totalEarnings.gold += result.gold; } if (result.loot) { addToInventory(result.loot, 1); agent.totalEarnings.items.push({ item: result.loot, amount: 1 }); } }); } // Process discoveries (for scouts/explorers) if (decision.discoveries) { decision.discoveries.forEach(d => { agent.results.push({ discovery: d.description }); }); } // Process healing if (decision.heal_amount) { gameData.player.hp = Math.min(gameData.player.maxHp, gameData.player.hp + decision.heal_amount); updateHealthUI(); agent.results.push({ heal: decision.heal_amount }); } // Process catch (fishing) if (decision.catch) { decision.catch.forEach(c => { if (c.item && c.amount) { addToInventory(c.item, c.amount); agent.totalEarnings.items.push({ item: c.item, amount: c.amount }); agent.results.push({ item: c.item, amount: c.amount }); } }); } // Process ore (mining) if (decision.ore) { decision.ore.forEach(o => { if (o.item && o.amount) { addToInventory(o.item, o.amount); agent.totalEarnings.items.push({ item: o.item, amount: o.amount }); agent.results.push({ item: o.item, amount: o.amount }); } }); } // Update progress based on time const elapsed = performance.now() - agent.spawnTime; agent.progress = Math.min(100, (elapsed / 60000) * 100); // 100% at 1 minute saveGameData(); } // v5.17: Simulate agent decision with efficiency system (fallback when no API) function simulateAgentDecision(agent, context) { const taskType = agent.typeConfig.taskType; // v5.17: Use efficiency-modified success rates const rand = Math.random(); let result = {}; let success = false; switch (taskType) { case 'gather': // v5.17: Base rate 0.4, modified by efficiency const gatherRate = getAgentSuccessRate(agent, 0.4); if (rand < gatherRate) { const items = ['Logs', 'Fiber', 'Stone', 'Herbs']; // v5.17: Amount scales with agent level const bonusAmount = Math.floor(agent.agentLevel / 3); result = { item: items[Math.floor(Math.random() * items.length)], amount: Math.floor(Math.random() * 2) + 1 + bonusAmount }; addToInventory(result.item, result.amount); agent.totalEarnings.items.push(result); agent.results.push(result); success = true; if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); } agent.statusMessage = success ? `Found ${result.amount} ${result.item}!` : 'Searching for resources...'; trackAgentAction(agent, success, 5); break; case 'hunt': // v5.17: Base rate 0.3, modified by efficiency const huntRate = getAgentSuccessRate(agent, 0.3); if (rand < huntRate) { // v5.17: XP and gold scale with agent level result.xp = Math.floor(Math.random() * 20) + 10 + agent.agentLevel * 2; result.gold = Math.floor(Math.random() * 10) + 3 + agent.agentLevel; if (typeof addXp === 'function') addXp('combat', result.xp); gameData.gold = (gameData.gold || 0) + result.gold; agent.totalEarnings.xp += result.xp; agent.totalEarnings.gold += result.gold; agent.results.push(result); success = true; if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); } agent.statusMessage = success ? `Defeated enemy! +${result.xp} XP` : 'Hunting enemies...'; trackAgentAction(agent, success, 8); break; case 'scout': // v5.17: Base rate 0.25, modified by efficiency const scoutRate = getAgentSuccessRate(agent, 0.25); if (rand < scoutRate) { const discoveries = ['Found a resource deposit', 'Spotted enemy camp', 'Discovered safe path', 'Located point of interest']; const disc = discoveries[Math.floor(Math.random() * discoveries.length)]; agent.results.push({ discovery: disc }); agent.statusMessage = disc; success = true; if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); } else { agent.statusMessage = 'Surveying the area...'; } trackAgentAction(agent, success, 4); break; case 'protect': const nearbyEnemies = countNearbyEnemies(agent); // v5.17: Base rate 0.5, modified by efficiency const protectRate = getAgentSuccessRate(agent, 0.5); if (nearbyEnemies > 0 && rand < protectRate) { agent.statusMessage = `Engaging ${nearbyEnemies} threat(s)!`; // v5.17: Damage scales with agent level const damage = 8 + agent.agentLevel * 2; if (worldState.mobs) { // v7.80: distanceToSquared optimization (15*15=225) const nearMob = worldState.mobs.find(m => m.mesh && m.mesh.position.distanceToSquared(agent.position) < 225); if (nearMob) { nearMob.hp -= damage; if (agent.mesh) { spawnFloater(agent.mesh.position, `-${damage}`, agent.typeConfig.color.toString(16)); spawnAgentParticleEffect(agent, 'success'); } success = true; } } } else { agent.statusMessage = nearbyEnemies > 0 ? 'Alert! Enemies nearby.' : 'Area secure.'; } trackAgentAction(agent, success, 6); break; case 'heal': // v5.17: Base rate 0.5, modified by efficiency const healRate = getAgentSuccessRate(agent, 0.5); if (gameData.player.hp < gameData.player.maxHp && rand < healRate) { // v5.17: Heal amount scales with agent level const healAmt = Math.floor(Math.random() * 8) + 5 + agent.agentLevel * 2; gameData.player.hp = Math.min(gameData.player.maxHp, gameData.player.hp + healAmt); updateHealthUI(); agent.results.push({ heal: healAmt }); agent.statusMessage = `Healed player for ${healAmt} HP!`; success = true; if (agent.mesh) spawnAgentParticleEffect(agent, 'heal'); } else { agent.statusMessage = gameData.player.hp < gameData.player.maxHp ? 'Channeling healing energy...' : 'Player at full health.'; } trackAgentAction(agent, success, 5); break; case 'fish': // v5.17: Base rate 0.3, modified by efficiency const fishRate = getAgentSuccessRate(agent, 0.3); if (rand < fishRate) { // v5.17: Chance for rare fish at higher levels const fishTypes = ['Raw Fish', 'Raw Fish', 'Raw Fish']; if (agent.agentLevel >= 3) fishTypes.push('Large Fish'); if (agent.agentLevel >= 5) fishTypes.push('Golden Fish'); result = { item: fishTypes[Math.floor(Math.random() * fishTypes.length)], amount: 1 }; addToInventory(result.item, result.amount); agent.totalEarnings.items.push(result); agent.results.push(result); success = true; agent.statusMessage = result.item === 'Golden Fish' ? '🌟 Caught a Golden Fish!' : 'Caught a fish!'; if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); } else { agent.statusMessage = rand < 0.5 ? 'Waiting for a bite...' : 'Casting line...'; } trackAgentAction(agent, success, 4); break; case 'mine': // v5.17: Base rate 0.35, modified by efficiency const mineRate = getAgentSuccessRate(agent, 0.35); if (rand < mineRate) { // v5.17: Better ores at higher levels const ores = ['Iron Ore', 'Copper Ore', 'Stone']; if (agent.agentLevel >= 3) ores.push('Silver Ore'); if (agent.agentLevel >= 5) ores.push('Gold Ore'); if (agent.agentLevel >= 7) ores.push('Crystal'); const bonusAmount = Math.floor(agent.agentLevel / 4); result = { item: ores[Math.floor(Math.random() * ores.length)], amount: Math.floor(Math.random() * 2) + 1 + bonusAmount }; addToInventory(result.item, result.amount); agent.totalEarnings.items.push(result); agent.results.push(result); success = true; agent.statusMessage = `Mined ${result.amount} ${result.item}!`; if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); } else { agent.statusMessage = 'Mining deposit...'; } trackAgentAction(agent, success, 6); break; // v6.10: INTELLIGENT TERRAFORMER - Seeks clear areas and smooths rough terrain for building // v6.33: Increased base rate from 0.4 to 0.7 for more reliable terraforming case 'terraform': const terraformRate = getAgentSuccessRate(agent, 0.7); if (rand < terraformRate && agent.mesh) { // v6.10: Intelligent terrain scanning and site selection const agentX = Math.floor((agent.mesh.position.x / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); const agentZ = Math.floor((agent.mesh.position.z / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); // ======================== // INTELLIGENT SITE SCANNER // ======================== // Calculate terrain roughness in an area (higher = more uneven = needs smoothing) const calculateRoughness = (cx, cz, radius) => { let heights = []; for (let dx = -radius; dx <= radius; dx++) { for (let dz = -radius; dz <= radius; dz++) { const tx = cx + dx, tz = cz + dz; if (worldState.terrain[tx] && worldState.terrain[tx][tz] !== undefined && worldState.terrain[tx][tz] > 0) { heights.push(worldState.terrain[tx][tz]); } } } if (heights.length < 2) return 0; const avg = heights.reduce((a, b) => a + b, 0) / heights.length; const variance = heights.reduce((sum, h) => sum + Math.pow(h - avg, 2), 0) / heights.length; return Math.sqrt(variance); }; // Count obstacles (trees/rocks) in an area const countObstacles = (cx, cz, radius) => { const worldCenterX = (cx - CONFIG.WORLD_SIZE / 2) * CONFIG.TILE_SIZE; const worldCenterZ = (cz - CONFIG.WORLD_SIZE / 2) * CONFIG.TILE_SIZE; const searchRadius = radius * CONFIG.TILE_SIZE; let count = 0; for (const obj of worldState.interactables) { if (obj.userData && (obj.userData.type === 'tree' || obj.userData.type === 'rock')) { const dist = Math.sqrt( Math.pow(obj.position.x - worldCenterX, 2) + Math.pow(obj.position.z - worldCenterZ, 2) ); if (dist <= searchRadius) count++; } } return count; }; // Check if area is already terraformed const isAlreadyTerraformed = (cx, cz, minDist = 4) => { return worldState.terraformedAreas.some(a => Math.abs(a.x - cx) < minDist && Math.abs(a.z - cz) < minDist); }; // ============================= // FIND BEST BUILDING SITE (AI) // ============================= const findBestBuildingSite = () => { let bestSite = null; let bestScore = -Infinity; const scanRadius = 15; // Tiles around agent to scan const siteRadius = 3; // Size of potential building site for (let dx = -scanRadius; dx <= scanRadius; dx += 3) { for (let dz = -scanRadius; dz <= scanRadius; dz += 3) { const sx = agentX + dx; const sz = agentZ + dz; // Skip invalid positions if (sx < 2 || sx >= CONFIG.WORLD_SIZE - 2 || sz < 2 || sz >= CONFIG.WORLD_SIZE - 2) continue; // Skip water areas if (!worldState.terrain[sx] || worldState.terrain[sx][sz] <= 0) continue; // Skip already terraformed areas if (isAlreadyTerraformed(sx, sz)) continue; // Calculate scores const obstacles = countObstacles(sx, sz, siteRadius); const roughness = calculateRoughness(sx, sz, siteRadius); // Scoring: Prioritize clear areas with rough terrain // High roughness = needs smoothing (good) // Low obstacles = clear for building (very good) const clearBonus = obstacles === 0 ? 50 : (obstacles <= 2 ? 20 : -obstacles * 5); const roughBonus = roughness * 10; // More rough = more valuable to smooth const distancePenalty = Math.sqrt(dx * dx + dz * dz) * 0.5; const score = clearBonus + roughBonus - distancePenalty; // v6.33: Lowered roughness threshold from 0.5 to 0.2 for more terraforming action if (score > bestScore && roughness > 0.2) { bestScore = score; bestSite = { x: sx, z: sz, obstacles, roughness, score, worldX: (sx - CONFIG.WORLD_SIZE / 2) * CONFIG.TILE_SIZE, worldZ: (sz - CONFIG.WORLD_SIZE / 2) * CONFIG.TILE_SIZE }; } } } return bestSite; }; // Find the best site const bestSite = findBestBuildingSite(); // ================================ // INTELLIGENT SITE NAVIGATION & SMOOTHING // ================================ // If we found a good site and we're not there, navigate to it if (bestSite && !agent.terraformTarget) { const distToSite = Math.sqrt( Math.pow(agentX - bestSite.x, 2) + Math.pow(agentZ - bestSite.z, 2) ); if (distToSite > 2) { // Navigate to the best site agent.terraformTarget = bestSite; agent.targetPosition = new THREE.Vector3(bestSite.worldX, agent.mesh.position.y, bestSite.worldZ); agent.taskState.state = 'moving'; agent.taskState.targetPosition = agent.targetPosition.clone(); const siteQuality = bestSite.obstacles === 0 ? '🟢 CLEAR' : (bestSite.obstacles <= 2 ? '🟡 SOME OBSTACLES' : '🔴 OBSTRUCTED'); agent.statusMessage = `📍 Found building site (${bestSite.x}, ${bestSite.z}) - ${siteQuality}`; if (typeof addCopilotMessage === 'function' && bestSite.obstacles === 0) { addCopilotMessage(`🚜 ${agent.name} detected clear building site at (${bestSite.x}, ${bestSite.z}) - Roughness: ${bestSite.roughness.toFixed(1)}`, 'ai'); } break; } } // Clear terraformTarget if we arrived if (agent.terraformTarget) { const distToTarget = Math.sqrt( Math.pow(agentX - agent.terraformTarget.x, 2) + Math.pow(agentZ - agent.terraformTarget.z, 2) ); if (distToTarget <= 2) { agent.terraformTarget = null; agent.statusMessage = '🚜 Arrived at site - beginning terrain smoothing...'; } } // ================================ // ENHANCED 5x5 TERRAIN SMOOTHING // ================================ const smoothRadius = 2; // 5x5 area (radius of 2) let maxHeight = -Infinity, minHeight = Infinity; let totalHeight = 0, count = 0; let heightMap = []; for (let dx = -smoothRadius; dx <= smoothRadius; dx++) { for (let dz = -smoothRadius; dz <= smoothRadius; dz++) { const tx = agentX + dx, tz = agentZ + dz; if (worldState.terrain[tx] && worldState.terrain[tx][tz] !== undefined && worldState.terrain[tx][tz] > 0) { const h = worldState.terrain[tx][tz]; maxHeight = Math.max(maxHeight, h); minHeight = Math.min(minHeight, h); totalHeight += h; count++; heightMap.push({ tx, tz, h }); } } } if (count > 0) { const avgHeight = totalHeight / count; const variance = maxHeight - minHeight; const roughness = calculateRoughness(agentX, agentZ, smoothRadius); // v6.33: Lowered variance threshold from 1 to 0.3, roughness from 0.5 to 0.2 if (variance > 0.3 || roughness > 0.2) { // ================================ // SMOOTH OPERATION - Gradual averaging // v9.4: Increased smoothing strength for more visible effect // ================================ const smoothingStrength = 0.95; // How much to smooth (0-1) for (const cell of heightMap) { // Smooth towards average with strength factor const newHeight = cell.h + (avgHeight - cell.h) * smoothingStrength; worldState.terrain[cell.tx][cell.tz] = newHeight; } // v6.33: Update the visual terrain meshes to match the smoothed data if (typeof worldState.updateTerrainMeshes === 'function') { worldState.updateTerrainMeshes(agentX, agentZ, smoothRadius); } // Record terraformed area with metadata const existingArea = worldState.terraformedAreas.find(a => Math.abs(a.x - agentX) < 4 && Math.abs(a.z - agentZ) < 4); if (!existingArea) { const obstacles = countObstacles(agentX, agentZ, smoothRadius); const newTerraformedArea = { x: agentX, z: agentZ, flatness: 100, createdAt: Date.now(), createdBy: agent.name, size: (smoothRadius * 2 + 1) + 'x' + (smoothRadius * 2 + 1), clearance: obstacles === 0 ? 'clear' : 'partial', avgHeight: avgHeight.toFixed(2), originalRoughness: roughness.toFixed(2) }; worldState.terraformedAreas.push(newTerraformedArea); // v6.11: Spawn construction site beacon for CLEAR areas if (obstacles === 0 && typeof createConstructionSiteBeacon === 'function') { createConstructionSiteBeacon(newTerraformedArea); if (typeof addCopilotMessage === 'function') { addCopilotMessage(`🏗️ ${agent.name} deployed CONSTRUCTION BEACON at (${agentX}, ${agentZ}) - Builder agents notified!`, 'ai'); } } else if (obstacles === 0 && typeof addCopilotMessage === 'function') { addCopilotMessage(`✨ ${agent.name} prepared a CLEAR 5x5 building site at (${agentX}, ${agentZ})!`, 'ai'); } } success = true; const siteStatus = countObstacles(agentX, agentZ, smoothRadius) === 0 ? '✨ READY FOR BUILDING' : '🚧 Smoothed'; agent.statusMessage = `🚜 ${siteStatus} at (${agentX}, ${agentZ})`; agent.results.push({ terraformed: { x: agentX, z: agentZ, flatness: 100, size: '5x5', clear: countObstacles(agentX, agentZ, smoothRadius) === 0, originalRoughness: roughness.toFixed(2) } }); if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); } else { // Area already flat - seek new site agent.statusMessage = '🔍 Area already smooth, scanning for rough terrain...'; // Try to find a new site if (bestSite) { agent.targetPosition = new THREE.Vector3(bestSite.worldX, agent.mesh.position.y, bestSite.worldZ); agent.taskState.state = 'moving'; agent.taskState.targetPosition = agent.targetPosition.clone(); agent.statusMessage = `📍 Relocating to (${bestSite.x}, ${bestSite.z})...`; } else { // No good sites nearby - wander to explore moveAgentToRandomPosition(agent); agent.statusMessage = '🔍 Exploring for uneven terrain...'; } } } else { agent.statusMessage = '📡 Scanning terrain topology...'; } } else { // Not performing action - show scanning status const scanMessages = [ '📡 Analyzing terrain data...', '🛰️ Scanning for clear areas...', '🗺️ Mapping terrain roughness...', '📊 Calculating optimal sites...' ]; agent.statusMessage = scanMessages[Math.floor(Math.random() * scanMessages.length)]; } trackAgentAction(agent, success, 8); break; // v6.11: INTELLIGENT Builder - seeks construction sites and builds optimal structures case 'build': const buildRate = getAgentSuccessRate(agent, 0.35); if (rand < buildRate && agent.mesh) { const agentX = Math.floor((agent.mesh.position.x / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); const agentZ = Math.floor((agent.mesh.position.z / CONFIG.TILE_SIZE) + CONFIG.WORLD_SIZE / 2); // ================================ // v6.11: INTELLIGENT SITE SEEKING // ================================ // First, check if there's a construction site beacon to navigate to if (!agent.targetConstructionSite && typeof findNearestConstructionSite === 'function') { const nearestSite = findNearestConstructionSite(agentX, agentZ); if (nearestSite) { const distToSite = Math.sqrt( Math.pow(agentX - nearestSite.x, 2) + Math.pow(agentZ - nearestSite.z, 2) ); if (distToSite > 2) { // Claim and navigate to the site nearestSite.claimedBy = agent.name; agent.targetConstructionSite = nearestSite; agent.targetPosition = new THREE.Vector3(nearestSite.worldX, agent.mesh.position.y, nearestSite.worldZ); agent.taskState.state = 'moving'; agent.taskState.targetPosition = agent.targetPosition.clone(); agent.statusMessage = `🎯 Navigating to construction beacon at (${nearestSite.x}, ${nearestSite.z})`; if (typeof addCopilotMessage === 'function') { addCopilotMessage(`🔧 ${agent.name} claimed construction site at (${nearestSite.x}, ${nearestSite.z})!`, 'ai'); } break; } } } // Check if we arrived at target construction site if (agent.targetConstructionSite) { const distToTarget = Math.sqrt( Math.pow(agentX - agent.targetConstructionSite.x, 2) + Math.pow(agentZ - agent.targetConstructionSite.z, 2) ); if (distToTarget <= 2) { agent.statusMessage = '🔧 Arrived at construction site - beginning build...'; } } // Check if on terraformed/flat area for efficiency bonus const onFlatArea = worldState.terraformedAreas.some(a => Math.abs(a.x - agentX) < 2 && Math.abs(a.z - agentZ) < 2); // Check if at a construction site beacon const atConstructionSite = worldState.constructionSites?.some(s => Math.abs(s.x - agentX) < 2 && Math.abs(s.z - agentZ) < 2); // Check if charger already exists nearby const nearbyCharger = worldState.structures.find(s => s.type === 'battery_charger' && Math.abs(s.x - agentX) < 5 && Math.abs(s.z - agentZ) < 5); if (!nearbyCharger) { // Build a new battery charger const efficiency = (onFlatArea || atConstructionSite) ? 100 : 60 + Math.floor(Math.random() * 20); const charger = createBatteryCharger( agent.mesh.position.x, agent.mesh.position.y, agent.mesh.position.z, efficiency ); if (charger) { success = true; // v6.11: Remove construction beacon if we built on it if (atConstructionSite && typeof removeConstructionSiteBeacon === 'function') { const siteToRemove = worldState.constructionSites?.find(s => Math.abs(s.x - agentX) < 2 && Math.abs(s.z - agentZ) < 2); if (siteToRemove) { removeConstructionSiteBeacon(siteToRemove); agent.statusMessage = `🏗️ Built OPTIMAL Charger on prepared site!`; if (typeof addCopilotMessage === 'function') { addCopilotMessage(`⚡ ${agent.name} completed construction at beacon site - 100% efficiency!`, 'ai'); } } } else { agent.statusMessage = `🔧 Built Battery Charger (${efficiency}% efficiency)!`; if (onFlatArea && typeof addCopilotMessage === 'function') { addCopilotMessage(`⚡ ${agent.name} built an optimal charger on flat terrain!`, 'ai'); } } agent.results.push({ built: { type: 'battery_charger', efficiency: efficiency, flat: onFlatArea, atBeacon: atConstructionSite } }); if (agent.mesh) spawnAgentParticleEffect(agent, 'success'); // Clear target construction site agent.targetConstructionSite = null; } } else { // Repair/upgrade existing charger if (nearbyCharger.efficiency < 100 && onFlatArea) { nearbyCharger.efficiency = Math.min(100, nearbyCharger.efficiency + 10); success = true; agent.statusMessage = `🔧 Upgraded charger to ${nearbyCharger.efficiency}%!`; } else { // Look for construction sites instead of random movement if (typeof findNearestConstructionSite === 'function') { const newSite = findNearestConstructionSite(agentX, agentZ); if (newSite) { newSite.claimedBy = agent.name; agent.targetConstructionSite = newSite; agent.targetPosition = new THREE.Vector3(newSite.worldX, agent.mesh.position.y, newSite.worldZ); agent.taskState.state = 'moving'; agent.taskState.targetPosition = agent.targetPosition.clone(); agent.statusMessage = `🎯 Found new construction beacon at (${newSite.x}, ${newSite.z})`; } else { agent.statusMessage = '🔍 Searching for construction sites...'; moveAgentToRandomPosition(agent); } } else { agent.statusMessage = 'Charger nearby, relocating...'; moveAgentToRandomPosition(agent); } } } } else { // Scanning status messages const scanMessages = [ '📡 Scanning for construction beacons...', '🔍 Searching for prepared sites...', '🗺️ Analyzing terrain data...', '📊 Calculating build priorities...' ]; agent.statusMessage = scanMessages[Math.floor(Math.random() * scanMessages.length)]; } trackAgentAction(agent, success, 10); break; } const elapsed = performance.now() - agent.spawnTime; agent.progress = Math.min(100, (elapsed / 60000) * 100); saveGameData(); updateAgentCardUI(agent); } // v5.17: Agent Experience and Efficiency System // XP required for each level (exponential curve) function getAgentXPForLevel(level) { return Math.floor(50 * Math.pow(1.5, level - 1)); } // Grant XP to an agent and handle level ups function grantAgentXP(agent, amount) { if (!agent) return; agent.agentXP += amount; // Check for level up let xpNeeded = getAgentXPForLevel(agent.agentLevel); while (agent.agentXP >= xpNeeded && agent.agentLevel < 10) { agent.agentXP -= xpNeeded; agent.agentLevel++; agent.efficiency = 1.0 + (agent.agentLevel - 1) * 0.1; // +10% efficiency per level // Level up effects if (agent.mesh) { spawnAgentParticleEffect(agent, 'levelup'); } addCopilotMessage(`🎉 ${agent.typeConfig.icon} ${agent.name} reached Level ${agent.agentLevel}! Efficiency: ${Math.round(agent.efficiency * 100)}%`, 'ai'); xpNeeded = getAgentXPForLevel(agent.agentLevel); } } // Track action success and update combo function trackAgentAction(agent, success, xpAmount = 5) { agent.actionsPerformed++; if (success) { agent.successfulActions++; agent.combo++; agent.maxCombo = Math.max(agent.maxCombo, agent.combo); agent.lastActionSuccess = true; // Combo XP bonus: +1 XP per combo level (max +10) const comboBonus = Math.min(agent.combo, 10); grantAgentXP(agent, xpAmount + comboBonus); // Spawn combo particle effect at milestones if (agent.combo === 5 || agent.combo === 10 || agent.combo === 25 || agent.combo % 50 === 0) { if (agent.mesh) { spawnAgentParticleEffect(agent, 'combo'); } if (agent.combo >= 10) { addCopilotMessage(`🔥 ${agent.typeConfig.icon} ${agent.name} ${agent.combo}x COMBO!`, 'ai'); } } } else { // Reset combo on failure if (agent.combo >= 5) { // Lost significant combo - notify player agent.statusMessage = `Combo lost at ${agent.combo}x`; } agent.combo = 0; agent.lastActionSuccess = false; // Still grant minimal XP for effort grantAgentXP(agent, 1); } } // Get effective success rate based on agent efficiency and combo function getAgentSuccessRate(agent, baseRate) { // Efficiency from level let rate = baseRate * agent.efficiency; // Combo bonus: up to +20% at 10+ combo const comboBonus = Math.min(agent.combo, 10) * 0.02; rate += comboBonus; // v5.17: Synergy bonus when agents work near each other const synergyBonus = getAgentSynergyBonus(agent); rate += synergyBonus; // Cap at 95% return Math.min(0.95, rate); } // v5.17: Calculate synergy bonus based on nearby allied agents // v8.03: Converted forEach to for loop for performance function getAgentSynergyBonus(agent) { if (!agent.mesh) return 0; let nearbyAgents = 0; const synergyRange = 15; // Units const synergyRangeSq = synergyRange * synergyRange; // v7.80: distanceToSquared optimization for (let i = 0, len = agentFleet.length; i < len; i++) { const other = agentFleet[i]; if (other.id === agent.id || !other.mesh) continue; const distSq = agent.mesh.position.distanceToSquared(other.mesh.position); if (distSq <= synergyRangeSq) { nearbyAgents++; } } // +5% per nearby agent, max +15% (3 agents) return Math.min(nearbyAgents, 3) * 0.05; } // v5.17: Particle effects for agent actions function spawnAgentParticleEffect(agent, effectType) { if (!agent.mesh || !scene) return; const particleCount = effectType === 'levelup' ? 20 : (effectType === 'combo' ? 12 : 8); const particleGroup = new THREE.Group(); // Choose color based on effect type let color; switch (effectType) { case 'levelup': color = 0xffd700; // Gold break; case 'combo': color = 0xff8800; // Orange break; case 'success': color = agent.typeConfig.color; break; case 'heal': color = 0x00ff88; break; default: color = 0xffffff; } // Create particles for (let i = 0; i < particleCount; i++) { const size = 0.05 + Math.random() * 0.1; const geom = new THREE.SphereGeometry(size, 4, 4); const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.9 }); const particle = new THREE.Mesh(geom, mat); // Random spread particle.position.set( (Math.random() - 0.5) * 0.5, 0.5 + Math.random() * 0.5, (Math.random() - 0.5) * 0.5 ); // Store velocity for animation particle.userData.velocity = new THREE.Vector3( (Math.random() - 0.5) * 2, 1 + Math.random() * 2, (Math.random() - 0.5) * 2 ); particleGroup.add(particle); } particleGroup.position.copy(agent.mesh.position); scene.add(particleGroup); // Animate and remove let elapsed = 0; const duration = effectType === 'levelup' ? 1500 : 800; // v7.84: Pre-allocated temp vector for velocity calculations in animation loop const _tempVelocity = new THREE.Vector3(); function animateParticles() { // v8.34: Skip animation when tab is hidden if (!isPageVisible) { requestAnimationFrame(animateParticles); return; } elapsed += 16; const progress = elapsed / duration; // v8.17: forEach-to-for loop conversion for particle cleanup (hot path) if (progress >= 1) { scene.remove(particleGroup); const particles = particleGroup.children; for (let pi = 0, plen = particles.length; pi < plen; pi++) { particles[pi].geometry.dispose(); particles[pi].material.dispose(); } return; } // v8.17: forEach-to-for loop conversion for particle animation (hot path) const animParticles = particleGroup.children; for (let pi = 0, plen = animParticles.length; pi < plen; pi++) { const p = animParticles[pi]; // v7.84: Use pre-allocated temp vector instead of clone() per particle per frame _tempVelocity.copy(p.userData.velocity).multiplyScalar(0.016); p.position.add(_tempVelocity); p.userData.velocity.y -= 3 * 0.016; // Gravity p.material.opacity = 0.9 * (1 - progress); p.scale.setScalar(1 - progress * 0.5); } requestAnimationFrame(animateParticles); } requestAnimationFrame(animateParticles); } // v5.17: Agent health regeneration (passive) function updateAgentHealthRegen(agent) { if (!agent || !agent.taskState) return; const now = performance.now(); const regenInterval = 5000; // Regen every 5 seconds if (now - agent.lastHealthRegen >= regenInterval) { agent.lastHealthRegen = now; const task = agent.taskState; if (task.hp < task.maxHp) { // Regen rate: 2 HP base + 1 HP per level const regenAmount = 2 + agent.agentLevel; task.hp = Math.min(task.maxHp, task.hp + regenAmount); // Show heal effect for significant heals if (regenAmount >= 3 && agent.mesh) { spawnAgentParticleEffect(agent, 'heal'); } } } } // Recall an agent function recallAgent(agentId) { const agentIndex = agentFleet.findIndex(a => a.id === agentId); if (agentIndex === -1) return; const agent = agentFleet[agentIndex]; // Stop the update loop if (agentUpdateTimers[agent.id]) { clearTimeout(agentUpdateTimers[agent.id]); delete agentUpdateTimers[agent.id]; } // Remove mesh from scene // v10.5: AGENT MESH DISPOSAL FIX (8-Agent Consensus Cycle 6) // Properly dispose all geometries and materials to prevent GPU memory leaks if (agent.mesh && scene) { scene.remove(agent.mesh); // Recursively dispose all children's geometry and materials agent.mesh.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (Array.isArray(child.material)) { child.material.forEach(m => m.dispose()); } else { child.material.dispose(); } } }); agent.mesh = null; } // Generate summary const summary = []; // v5.17: Include level and stats in summary summary.push(`Lv.${agent.agentLevel}`); if (agent.maxCombo > 0) summary.push(`Best combo: ${agent.maxCombo}x`); if (agent.totalEarnings.xp > 0) summary.push(`+${agent.totalEarnings.xp} XP`); if (agent.totalEarnings.gold > 0) summary.push(`+${agent.totalEarnings.gold} Gold`); if (agent.totalEarnings.items.length > 0) { const itemCounts = {}; agent.totalEarnings.items.forEach(i => { itemCounts[i.item] = (itemCounts[i.item] || 0) + (i.amount || 1); }); Object.entries(itemCounts).forEach(([item, count]) => { summary.push(`+${count} ${item}`); }); } // Remove from fleet agentLookup.delete(agent.id); // v8.18: Remove from lookup Map agentFleet.splice(agentIndex, 1); // Announce // v5.17: Enhanced recall message with agent performance stats const successRate = agent.actionsPerformed > 0 ? Math.round((agent.successfulActions / agent.actionsPerformed) * 100) : 0; const statsStr = `[${successRate}% success rate, ${agent.actionsPerformed} actions]`; const summaryStr = summary.length > 0 ? ` ${summary.join(', ')}` : ''; addCopilotMessage(`${agent.typeConfig.icon} ${agent.name} recalled! ${statsStr}${summaryStr}`, 'ai'); updateFleetUI(); updateFleetButton(); } // Update the fleet button indicator function updateFleetButton() { const btn = document.getElementById('fleet-button'); if (agentFleet.length > 0) { btn.classList.add('has-agents'); btn.setAttribute('data-count', agentFleet.length); } else { btn.classList.remove('has-agents'); } } // Update the fleet panel UI function updateFleetUI() { // v6.3.5: Handle page refresh signal events for agents // This is called whenever agents are updated, so it's a good place to trigger refresh handling if (typeof SignalInterruptionSystem !== 'undefined' && SignalInterruptionSystem.pageWasRefreshed && !SignalInterruptionSystem.refreshHandled && agentFleet.length > 0) { // Delay slightly to let UI settle setTimeout(() => { SignalInterruptionSystem.handlePageRefresh(agentFleet); }, 500); } document.getElementById('fleet-count').textContent = `${agentFleet.length}/${MAX_AGENTS}`; // Update spawn buttons disabled state const spawnBtns = document.querySelectorAll('.agent-spawn-btn'); spawnBtns.forEach(btn => { btn.disabled = agentFleet.length >= MAX_AGENTS; }); // Update agent list const listContainer = document.getElementById('agent-fleet-list'); if (agentFleet.length === 0) { listContainer.innerHTML = `
No agents deployed yet.
Click an agent type above to spawn.
`; return; } listContainer.innerHTML = agentFleet.map(agent => { const colorHex = '#' + agent.typeConfig.color.toString(16).padStart(6, '0'); const statusDotClass = agent.status === 'thinking' ? 'thinking' : (agent.status === 'idle' ? 'idle' : ''); // v5.14: Get endpoint/profile info const agentEndpoint = getAgentEndpoint(agent); const endpointBadge = agent.profileId ? `${agentEndpoint.name}` : ''; const recentResults = agent.results.slice(-3).map(r => { if (r.item) return `+${r.amount} ${r.item}`; if (r.xp) return `+${r.xp} XP`; if (r.gold) return `+${r.gold} Gold`; if (r.heal) return `+${r.heal} HP`; if (r.discovery) return r.discovery.substring(0, 20); return ''; }).filter(Boolean); return `
${agent.typeConfig.icon}
${agent.name}${endpointBadge}
${agent.typeConfig.name}
${agent.statusMessage}
${recentResults.length > 0 ? `
${recentResults.map(r => `${r}`).join('')}
` : ''}
Live Transcript ${agent.conversationHistory.length} msgs ${agent.status === 'thinking' ? '' : ''}
${buildAgentTranscriptHTML(agent)}
`; }).join(''); } // Update a single agent card UI // v5.16.3: Enhanced to show actual task state from autonomous system function updateAgentCardUI(agent) { const card = document.querySelector(`.agent-card[data-agent-id="${agent.id}"]`); if (!card) return; const statusDot = card.querySelector('.agent-status-dot'); const statusText = card.querySelector('.status-text'); const progressBar = card.querySelector('.agent-progress-bar'); if (statusDot) { statusDot.className = 'agent-status-dot'; if (agent.status === 'thinking') statusDot.classList.add('thinking'); if (agent.status === 'idle') statusDot.classList.add('idle'); } // v6.3.1: Show actual task state - always local/deterministic mode let displayStatus = agent.statusMessage; const modeIndicator = '⚡'; // Always local mode now if (agent.mesh && agent.taskState) { const task = agent.taskState; // v6.4.0: Added returning/depositing states for hauling const invCount = task.inventory?.length || 0; const invCap = task.carryingCapacity || 6; const stateDescriptions = { 'idle': 'Scanning for targets...', 'moving': `Moving to target`, 'working': 'Harvesting...', 'combat': 'In combat!', 'alert': task.alert || 'Alert!', 'manual_control': 'Manual control', 'stuck': 'Repositioning...', 'returning': `Returning to ship [${invCount}/${invCap}]`, 'depositing': `Depositing resources...` }; const stateMsg = stateDescriptions[task.state] || task.state; const pos = agent.mesh.position; displayStatus = `${modeIndicator} ${stateMsg} (${Math.floor(pos.x)}, ${Math.floor(pos.z)})`; } else if (!agent.mesh) { // v6.3.0: Changed from "Waiting to spawn..." - agent is working even before mesh // Show that work is happening even without visual representation displayStatus = `${modeIndicator} ${agent.statusMessage || 'Working (no visual yet)'}`; } if (statusText) statusText.textContent = displayStatus; if (progressBar) progressBar.style.width = `${agent.progress}%`; // v5.17: Update level and combo display let levelComboDiv = card.querySelector('.agent-level-combo'); if (!levelComboDiv) { levelComboDiv = document.createElement('div'); levelComboDiv.className = 'agent-level-combo'; levelComboDiv.style.cssText = 'display: flex; gap: 8px; margin: 4px 0; font-size: 10px;'; const statusContainer = card.querySelector('.agent-card-status'); if (statusContainer) statusContainer.after(levelComboDiv); } const xpNeeded = getAgentXPForLevel(agent.agentLevel); const xpPercent = Math.min(100, Math.round((agent.agentXP / xpNeeded) * 100)); const comboColor = agent.combo >= 10 ? '#ff8800' : (agent.combo >= 5 ? '#ffcc00' : '#888'); const levelColor = agent.agentLevel >= 5 ? '#ffd700' : (agent.agentLevel >= 3 ? '#00ff88' : '#0ff'); levelComboDiv.innerHTML = ` Lv.${agent.agentLevel} (${xpPercent}%) ${agent.combo > 0 ? `🔥 ${agent.combo}x` : ''} ⚡${Math.round(agent.efficiency * 100)}% `; // v6.4.0: Add inventory display for gatherer/miner agents const task = agent.taskState; if (task && task.inventory !== undefined && (agent.type === 'gatherer' || agent.type === 'miner')) { let invDiv = card.querySelector('.agent-inventory-display'); if (!invDiv) { invDiv = document.createElement('div'); invDiv.className = 'agent-inventory-display'; invDiv.style.cssText = 'display: flex; align-items: center; gap: 4px; margin: 4px 0; font-size: 10px; flex-wrap: wrap;'; levelComboDiv.after(invDiv); } const invCount = task.inventory?.length || 0; const invCap = task.carryingCapacity || 6; const invPercent = Math.round((invCount / invCap) * 100); const invColor = invCount >= invCap ? '#ff8800' : (invCount > invCap / 2 ? '#ffcc00' : '#0ff'); const trips = task.tripsCompleted || 0; const hauled = task.totalHauled || 0; // Group inventory items const itemCounts = {}; (task.inventory || []).forEach(item => { itemCounts[item] = (itemCounts[item] || 0) + 1; }); const itemSummary = Object.entries(itemCounts).map(([item, count]) => `${count}x ${item.substring(0,8)}` ).join(''); invDiv.innerHTML = ` 📦 ${invCount}/${invCap} ${trips > 0 ? `🚀${trips}` : ''} ${hauled > 0 ? `📊${hauled}` : ''} ${itemSummary ? `
${itemSummary}
` : ''} `; } // Update results const recentResults = agent.results.slice(-3).map(r => { if (r.item) return `+${r.amount} ${r.item}`; if (r.xp) return `+${r.xp} XP`; if (r.gold) return `+${r.gold} Gold`; if (r.heal) return `+${r.heal} HP`; if (r.discovery) return r.discovery.substring(0, 20); return ''; }).filter(Boolean); let resultsDiv = card.querySelector('.agent-results-mini'); if (recentResults.length > 0) { if (!resultsDiv) { resultsDiv = document.createElement('div'); resultsDiv.className = 'agent-results-mini'; card.appendChild(resultsDiv); } resultsDiv.innerHTML = recentResults.map(r => `${r}`).join(''); } // v5.15: Update transcript toggle and viewer const transcriptToggle = card.querySelector('.agent-transcript-toggle'); if (transcriptToggle) { const msgCount = transcriptToggle.querySelector('.transcript-message-count'); if (msgCount) msgCount.textContent = `${agent.conversationHistory.length} msgs`; // Update live indicator let liveIndicator = transcriptToggle.querySelector('.transcript-live-indicator'); if (agent.status === 'thinking') { if (!liveIndicator) { liveIndicator = document.createElement('span'); liveIndicator.className = 'transcript-live-indicator'; transcriptToggle.appendChild(liveIndicator); } } else if (liveIndicator) { liveIndicator.remove(); } } // Update transcript viewer if expanded const transcriptViewer = card.querySelector('.agent-transcript-viewer.expanded'); if (transcriptViewer) { updateAgentTranscriptUI(agent); } } // v5.15: Toggle agent transcript viewer expansion // v5.16.1: Added body cam rendering when expanded function toggleAgentTranscript(agentId) { const card = document.querySelector(`.agent-card[data-agent-id="${agentId}"]`); if (!card) return; const toggle = card.querySelector('.agent-transcript-toggle'); const viewer = card.querySelector('.agent-transcript-viewer'); if (toggle && viewer) { const isExpanded = viewer.classList.contains('expanded'); toggle.classList.toggle('expanded', !isExpanded); viewer.classList.toggle('expanded', !isExpanded); // If opening, update the content and render body cam if (!isExpanded) { const agent = agentLookup.get(agentId); if (agent) { viewer.innerHTML = buildAgentTranscriptHTML(agent); // Scroll to bottom to show latest messages viewer.scrollTop = viewer.scrollHeight; // v5.16.1: Render body cam after DOM update setTimeout(() => renderAgentBodyCam(agent), 100); } } } } // v5.15: Build HTML for agent transcript messages // v5.15.2: Enhanced with Try Again replay buttons // v5.16.1: Added body cam preview function buildAgentTranscriptHTML(agent) { const history = agent.conversationHistory; if (history.length === 0) { return '
No messages yet - agent is initializing...
'; } const agentEndpoint = getAgentEndpoint(agent); const endpointInfo = agentEndpoint.name || 'Local Simulation'; const interactions = agent.interactionHistory || []; // v5.16.1: Get agent position and status for body cam overlay const agentPos = agent.mesh ? agent.mesh.position : { x: 0, y: 0, z: 0 }; const taskState = agent.taskState || {}; const stateText = taskState.state || 'initializing'; const currentAction = taskState.currentTask || stateText; let html = `
BODY CAM
${stateText.toUpperCase()}
X:${Math.floor(agentPos.x)} Z:${Math.floor(agentPos.z)}
${agent.typeConfig.icon} ${agent.statusMessage?.substring(0, 25) || 'Working...'}
📡 ${endpointInfo} ${history.length} msgs | ${interactions.length} interactions
`; // Show messages (system first, then alternating user/assistant) history.forEach((msg, idx) => { const roleClass = msg.role === 'system' ? 'system' : (msg.role === 'user' ? 'user' : 'assistant'); const roleIcon = msg.role === 'system' ? '⚙️' : (msg.role === 'user' ? '📤' : '🤖'); const roleLabel = msg.role === 'system' ? 'System' : (msg.role === 'user' ? 'Context' : 'Response'); // Truncate long content for display let content = msg.content; const isTruncated = content.length > 500; if (isTruncated) { content = content.substring(0, 500) + '... [truncated]'; } // Escape HTML entities content = content.replace(/&/g, '&').replace(//g, '>'); // v5.15.2: Find matching interaction for assistant responses let tryAgainBtn = ''; if (msg.role === 'assistant') { // Find the interaction that produced this response const interaction = interactions.find(i => i.response === msg.content || (i.conversationIndexAfter && i.conversationIndexAfter > idx) ); if (interaction) { const isReplaying = agent.replayState?.interactionId === interaction.id; tryAgainBtn = ` `; } } html += `
${roleIcon} ${roleLabel} #${idx + 1} ${tryAgainBtn}
${content}
`; }); // v5.15.2: Show replay comparison if active if (agent.replayState) { html += buildReplayComparisonHTML(agent); } html += '
'; return html; } // v5.15.2: Build HTML for replay comparison view function buildReplayComparisonHTML(agent) { const replay = agent.replayState; if (!replay) return ''; const originalContent = (replay.originalResponse || '').replace(/&/g, '&').replace(//g, '>'); const retryContent = (replay.retryResponse || 'Waiting for response...').replace(/&/g, '&').replace(//g, '>'); const isSame = replay.retryResponse && replay.originalResponse === replay.retryResponse; const isDifferent = replay.retryResponse && replay.originalResponse !== replay.retryResponse; return `
🔄 Try Again Comparison ${replay.retryResponse ? ` ${isSame ? '✓ Same Result' : '⚡ Different Result'} ` : '⏳ Replaying...'}
📜 Original Response
${originalContent.substring(0, 300)}${originalContent.length > 300 ? '...' : ''}
🆕 Retry Response
${retryContent.substring(0, 300)}${retryContent.length > 300 ? '...' : ''}
${replay.retryResponse ? `
${isDifferent ? ` ` : ''}
` : ''}
`; } // v5.15.2: Try Again - replay an interaction with current game state async function tryAgainInteraction(agentId, interactionId) { const agent = agentLookup.get(agentId); if (!agent) return; const interaction = agent.interactionHistory.find(i => i.id === interactionId); if (!interaction) { console.warn('Interaction not found:', interactionId); return; } // Set replay state agent.replayState = { interactionId: interactionId, originalResponse: interaction.response, originalContext: interaction.gameContext, retryResponse: null, retryContext: null, status: 'replaying' }; // Update UI to show replaying state updateAgentTranscriptUI(agent); // Get endpoint const endpoint = getAgentEndpoint(agent); if (!endpoint || !endpoint.url || !endpoint.key) { agent.replayState.retryResponse = '[ERROR: No endpoint configured]'; agent.replayState.status = 'error'; updateAgentTranscriptUI(agent); return; } try { // Build NEW context with current game state const newGameContext = buildGameContextForAgent(agent); agent.replayState.retryContext = newGameContext; // Build new context message const newContextMessage = { role: 'user', content: `Current situation: ${JSON.stringify(newGameContext)}. What's your next action?` }; // Use original conversation history up to that point const originalConversation = agent.conversationHistory.slice(0, interaction.conversationIndexBefore); const conversationForApi = [...originalConversation, newContextMessage]; // Build headers const headers = { 'Content-Type': 'application/json' }; if (endpoint.headerPrefix) { headers[endpoint.headerStyle] = endpoint.headerPrefix + endpoint.key; } else { headers[endpoint.headerStyle] = endpoint.key; } // Format request body const requestBody = formatAgentRequestBody(endpoint, newContextMessage, conversationForApi, agent); // Make API call const response = await fetch(endpoint.url, { method: 'POST', headers: headers, body: requestBody }); if (response.ok) { const data = await response.json(); const textResponse = parseAgentResponse(endpoint, data); agent.replayState.retryResponse = textResponse; agent.replayState.status = 'complete'; agent.replayState.retryRawResponse = data; } else { const errorText = await response.text().catch(() => ''); agent.replayState.retryResponse = `[API Error ${response.status}: ${errorText.substring(0, 100)}]`; agent.replayState.status = 'error'; } } catch (error) { console.error('Try Again error:', error); agent.replayState.retryResponse = `[Network Error: ${error.message}]`; agent.replayState.status = 'error'; } // Update UI with comparison updateAgentTranscriptUI(agent); } // v5.15.2: Dismiss replay comparison without changes function dismissReplayComparison(agentId) { const agent = agentLookup.get(agentId); if (!agent) return; agent.replayState = null; updateAgentTranscriptUI(agent); } // v5.15.2: Apply retry response and branch off with new conversation function applyRetryResponse(agentId) { const agent = agentLookup.get(agentId); if (!agent || !agent.replayState) return; const replay = agent.replayState; const interaction = agent.interactionHistory.find(i => i.id === replay.interactionId); if (!interaction || !replay.retryResponse) { dismissReplayComparison(agentId); return; } // Branch: Keep conversation up to original interaction point agent.conversationHistory = agent.conversationHistory.slice(0, interaction.conversationIndexBefore); // Add new context message with current game state const newContextMessage = { role: 'user', content: `Current situation: ${JSON.stringify(replay.retryContext)}. What's your next action?` }; agent.conversationHistory.push(newContextMessage); // Add retry response agent.conversationHistory.push({ role: 'assistant', content: replay.retryResponse }); // Create new interaction record for the branch const branchInteraction = { id: Date.now().toString(36) + Math.random().toString(36).substr(2, 5), timestamp: Date.now(), contextMessage: newContextMessage, gameContext: replay.retryContext, endpoint: interaction.endpoint, response: replay.retryResponse, rawResponse: replay.retryRawResponse, conversationIndexBefore: interaction.conversationIndexBefore, conversationIndexAfter: agent.conversationHistory.length, executed: false, branchedFrom: interaction.id // Track branch origin }; agent.interactionHistory.push(branchInteraction); // Execute the new decision parseAndExecuteAgentDecision(agent, replay.retryResponse); branchInteraction.executed = true; // Clear replay state agent.replayState = null; // Notify addCopilotMessage(`🔄 ${agent.name} branched off with new response from Try Again!`, 'ai'); // Update UI updateAgentCardUI(agent); updateAgentTranscriptUI(agent); } // v5.15: Update agent transcript UI (when new messages arrive) function updateAgentTranscriptUI(agent) { const viewer = document.getElementById(`transcript-viewer-${agent.id}`); if (!viewer || !viewer.classList.contains('expanded')) return; // Store scroll position const wasScrolledToBottom = viewer.scrollHeight - viewer.clientHeight <= viewer.scrollTop + 10; // Update content viewer.innerHTML = buildAgentTranscriptHTML(agent); // Auto-scroll to bottom if user was at bottom if (wasScrolledToBottom) { viewer.scrollTop = viewer.scrollHeight; } // v5.16.1: Render body cam after DOM update setTimeout(() => renderAgentBodyCam(agent), 50); } // v5.16.1: Agent body cam renderer and camera system // v5.16.3: Rewritten using streaming pattern from AI Companion Hub let agentBodyCamCamera = null; let agentBodyCamRenderer = null; let bodyCamInitialized = false; // Body cam streaming state (similar to Show Mode pattern) const bodyCamState = { activeStreams: new Map(), // agentId -> { lastRender: timestamp, canvas: element } updateInterval: 100, // ms between frame updates isRendering: false }; // v7.37: Pre-allocated vectors for Agent Camera System (Cycle 16 Performance) // Eliminates 4 THREE.Vector3 allocations per frame during agent view rendering const _agentCamOffset = new THREE.Vector3(); const _agentCamLookTarget = new THREE.Vector3(); // Initialize body cam rendering system (streaming pattern) function initAgentBodyCamSystem() { if (bodyCamInitialized) return true; if (!scene) { console.log('Body cam init: waiting for scene...'); return false; } try { // Create a dedicated camera for body cam views agentBodyCamCamera = new THREE.PerspectiveCamera(70, 320/150, 0.1, 150); // Create a dedicated renderer with preserveDrawingBuffer for canvas copy // This is the key fix - similar to AI Companion Hub's renderer setup agentBodyCamRenderer = new THREE.WebGLRenderer({ alpha: true, antialias: false, powerPreference: 'low-power', preserveDrawingBuffer: true // Critical for canvas streaming! }); agentBodyCamRenderer.setSize(320, 150); agentBodyCamRenderer.setPixelRatio(1); agentBodyCamRenderer.setClearColor(0x000000, 1); bodyCamInitialized = true; console.log('Body cam system initialized successfully'); return true; } catch (error) { console.error('Failed to initialize body cam system:', error); return false; } } // v5.16.3: Stream agent's POV to body cam canvas (AI Companion Hub pattern) function renderAgentBodyCam(agent) { if (!agent || !agent.mesh) { renderBodyCamPlaceholder(agent); return; } if (!scene) { renderBodyCamPlaceholder(agent, 'Waiting for world...'); return; } const canvas = document.getElementById(`bodycam-${agent.id}`); if (!canvas) return; // Initialize body cam system if needed if (!bodyCamInitialized && !initAgentBodyCamSystem()) { renderBodyCamPlaceholder(agent, 'Initializing...'); return; } if (!agentBodyCamRenderer || !agentBodyCamCamera) { renderBodyCamPlaceholder(agent, 'Renderer unavailable'); return; } try { // Position camera at agent's head level, looking in their facing direction const agentPos = agent.mesh.position; const agentRotation = agent.mesh.rotation.y || 0; // Camera position: at agent's eye level, looking forward agentBodyCamCamera.position.set( agentPos.x + Math.sin(agentRotation) * 0.3, agentPos.y + 1.5, // Eye level agentPos.z + Math.cos(agentRotation) * 0.3 ); // Look in the direction the agent is facing const lookTarget = new THREE.Vector3( agentPos.x + Math.sin(agentRotation) * 10, agentPos.y + 1.0, agentPos.z + Math.cos(agentRotation) * 10 ); agentBodyCamCamera.lookAt(lookTarget); // Render the scene from agent's perspective agentBodyCamRenderer.render(scene, agentBodyCamCamera); // Stream to the canvas element (AI Companion Hub pattern) const ctx = canvas.getContext('2d'); if (ctx) { // Clear and draw the rendered frame ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(agentBodyCamRenderer.domElement, 0, 0, canvas.width, canvas.height); // Add scan line effect for retro feel ctx.strokeStyle = 'rgba(0, 255, 255, 0.03)'; for (let y = 0; y < canvas.height; y += 3) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); } // Add vignette effect const gradient = ctx.createRadialGradient(canvas.width/2, canvas.height/2, 30, canvas.width/2, canvas.height/2, 180); gradient.addColorStop(0, 'rgba(0,0,0,0)'); gradient.addColorStop(1, 'rgba(0,0,0,0.4)'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // v5.17: Add level and combo HUD overlay ctx.font = 'bold 10px monospace'; // Level badge (top-left) const levelColor = agent.agentLevel >= 5 ? '#ffd700' : (agent.agentLevel >= 3 ? '#00ff88' : '#0ff'); ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(4, 4, 36, 14); ctx.fillStyle = levelColor; ctx.fillText(`Lv.${agent.agentLevel}`, 8, 14); // Combo indicator (top-right, if active) if (agent.combo >= 3) { const comboText = `${agent.combo}x`; const comboColor = agent.combo >= 10 ? '#ff8800' : (agent.combo >= 5 ? '#ffcc00' : '#fff'); const comboWidth = ctx.measureText(comboText).width + 16; ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(canvas.width - comboWidth - 4, 4, comboWidth, 14); ctx.fillStyle = comboColor; ctx.fillText(`🔥${comboText}`, canvas.width - comboWidth, 14); } // Efficiency bar (bottom-left) const effPercent = agent.efficiency * 100; const barWidth = 50; const barHeight = 4; ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(4, canvas.height - 10, barWidth + 4, barHeight + 4); ctx.fillStyle = '#333'; ctx.fillRect(6, canvas.height - 8, barWidth, barHeight); ctx.fillStyle = effPercent >= 150 ? '#ffd700' : (effPercent >= 120 ? '#00ff88' : '#0ff'); ctx.fillRect(6, canvas.height - 8, barWidth * (effPercent / 200), barHeight); // Update stream state bodyCamState.activeStreams.set(agent.id, { lastRender: performance.now(), canvas: canvas }); } } catch (error) { console.error(`Body cam render error for ${agent.name}:`, error); renderBodyCamPlaceholder(agent, 'Render error'); } } // ============================================ // v6.3.2: SIGNAL INTERRUPTION EVENT SYSTEM // v6.3.3: Added recovery mechanics and permanent loss // Realistic reasons for signal loss on refresh // Temporary events can recover, catastrophic = permanent // ============================================ const SignalInterruptionSystem = { // Active recovery timers per agent recoveryTimers: new Map(), // Agents with recovered signals ready to reconnect recoveredAgents: new Map(), // Permanently lost agents (catastrophic events) lostAgents: new Set(), // Comprehensive list of realistic interruption events // recoverable: true = can recover, false = permanent loss // recoveryTime: seconds until signal can be restored (min-max range) // recoveryChance: probability of successful recovery (0-1) events: [ // Environmental Hazards - All recoverable { id: 'sandstorm', category: 'environmental', title: 'SANDSTORM INTERFERENCE', message: 'Silicate particles blocking transmission', color: '#d4a574', icon: '🌪️', recovery: 'Awaiting storm subsidence...', recoverable: true, recoveryTime: [30, 90], recoveryChance: 0.95 }, { id: 'ion_storm', category: 'environmental', title: 'ION STORM', message: 'Electromagnetic interference detected', color: '#9966ff', icon: '⚡', recovery: 'Recalibrating antenna array...', recoverable: true, recoveryTime: [20, 60], recoveryChance: 0.90 }, { id: 'solar_flare', category: 'environmental', title: 'SOLAR FLARE EVENT', message: 'CME disrupting communications', color: '#ff6600', icon: '☀️', recovery: 'Switching to backup frequency...', recoverable: true, recoveryTime: [45, 120], recoveryChance: 0.85 }, { id: 'meteor_shower', category: 'environmental', title: 'METEOR SHOWER', message: 'Debris field blocking signal path', color: '#888899', icon: '☄️', recovery: 'Calculating clear window...', recoverable: true, recoveryTime: [15, 45], recoveryChance: 0.98 }, { id: 'dust_devil', category: 'environmental', title: 'DUST DEVIL CONTACT', message: 'Localized vortex disruption', color: '#aa8866', icon: '🌀', recovery: 'Waiting for conditions to clear...', recoverable: true, recoveryTime: [10, 30], recoveryChance: 0.99 }, { id: 'seismic', category: 'environmental', title: 'SEISMIC ACTIVITY', message: 'Ground vibrations destabilizing relay', color: '#665544', icon: '🌋', recovery: 'Stabilizing communication dish...', recoverable: true, recoveryTime: [25, 75], recoveryChance: 0.88 }, { id: 'radiation_burst', category: 'environmental', title: 'RADIATION BURST', message: 'High-energy particles saturating sensors', color: '#ff4488', icon: '☢️', recovery: 'Shielding electronics...', recoverable: true, recoveryTime: [40, 100], recoveryChance: 0.80 }, { id: 'magnetic_anomaly', category: 'environmental', title: 'MAGNETIC ANOMALY', message: 'Local field distorting signal', color: '#4488ff', icon: '🧲', recovery: 'Compensating for drift...', recoverable: true, recoveryTime: [20, 50], recoveryChance: 0.92 }, // Technical Issues - All recoverable with varying times { id: 'battery_low', category: 'technical', title: 'LOW POWER MODE', message: 'Battery reserves critically low', color: '#ff4444', icon: '🔋', recovery: 'Routing to solar recharge...', recoverable: true, recoveryTime: [60, 180], recoveryChance: 0.75 }, { id: 'antenna_damage', category: 'technical', title: 'ANTENNA MALFUNCTION', message: 'Physical damage to comm array', color: '#ff8800', icon: '📡', recovery: 'Deploying repair protocols...', recoverable: true, recoveryTime: [90, 240], recoveryChance: 0.65 }, { id: 'firmware_update', category: 'technical', title: 'FIRMWARE UPDATE', message: 'Critical system patch installing', color: '#00ff88', icon: '💾', recovery: 'Update progress: 73%...', recoverable: true, recoveryTime: [15, 45], recoveryChance: 0.99 }, { id: 'thermal_shutdown', category: 'technical', title: 'THERMAL PROTECTION', message: 'CPU temp exceeds safe limits', color: '#ff2200', icon: '🌡️', recovery: 'Engaging cooling systems...', recoverable: true, recoveryTime: [30, 90], recoveryChance: 0.85 }, { id: 'memory_overflow', category: 'technical', title: 'MEMORY OVERFLOW', message: 'Buffer limit reached, clearing cache', color: '#ffaa00', icon: '🧠', recovery: 'Garbage collection active...', recoverable: true, recoveryTime: [10, 25], recoveryChance: 0.98 }, { id: 'calibration', category: 'technical', title: 'SENSOR CALIBRATION', message: 'Recalibrating optical systems', color: '#00aaff', icon: '🔧', recovery: 'Alignment in progress...', recoverable: true, recoveryTime: [20, 60], recoveryChance: 0.95 }, { id: 'quantum_decoherence', category: 'technical', title: 'QUANTUM DECOHERENCE', message: 'Entangled relay lost coherence', color: '#ff00ff', icon: '⚛️', recovery: 'Re-establishing quantum link...', recoverable: true, recoveryTime: [45, 120], recoveryChance: 0.70 }, // Celestial Events - Recoverable but longer duration { id: 'eclipse', category: 'celestial', title: 'SOLAR ECLIPSE', message: 'Primary star occluded by moon', color: '#334455', icon: '🌑', recovery: 'Switching to backup power...', recoverable: true, recoveryTime: [120, 300], recoveryChance: 0.95 }, { id: 'planetary_alignment', category: 'celestial', title: 'PLANETARY ALIGNMENT', message: 'Gravitational lensing effect', color: '#6644aa', icon: '🪐', recovery: 'Adjusting for orbital drift...', recoverable: true, recoveryTime: [60, 180], recoveryChance: 0.88 }, { id: 'asteroid_shadow', category: 'celestial', title: 'ASTEROID SHADOW', message: 'Large body blocking relay satellite', color: '#445566', icon: '🌑', recovery: 'Waiting for orbital clearance...', recoverable: true, recoveryTime: [90, 240], recoveryChance: 0.92 }, { id: 'comet_tail', category: 'celestial', title: 'COMET TAIL TRANSIT', message: 'Ionized particles from passing comet', color: '#88ffff', icon: '☄️', recovery: 'Signal routing through debris...', recoverable: true, recoveryTime: [45, 120], recoveryChance: 0.85 }, { id: 'black_hole_lensing', category: 'celestial', title: 'GRAVITATIONAL LENSING', message: 'Nearby singularity bending signal', color: '#220033', icon: '🕳️', recovery: 'Compensating for spacetime curve...', recoverable: true, recoveryTime: [120, 300], recoveryChance: 0.60 }, // Atmospheric Conditions - All recoverable { id: 'acid_rain', category: 'atmospheric', title: 'ACID PRECIPITATION', message: 'Corrosive atmosphere degrading antenna', color: '#88ff44', icon: '🌧️', recovery: 'Deploying protective coating...', recoverable: true, recoveryTime: [30, 90], recoveryChance: 0.82 }, { id: 'methane_fog', category: 'atmospheric', title: 'METHANE FOG BANK', message: 'Dense hydrocarbon layer blocking IR', color: '#668844', icon: '🌫️', recovery: 'Switching to radio frequency...', recoverable: true, recoveryTime: [20, 60], recoveryChance: 0.90 }, { id: 'ammonia_clouds', category: 'atmospheric', title: 'AMMONIA CLOUD LAYER', message: 'Chemical interference in upper atmo', color: '#aabbcc', icon: '☁️', recovery: 'Boosting signal strength...', recoverable: true, recoveryTime: [25, 75], recoveryChance: 0.88 }, { id: 'lightning_storm', category: 'atmospheric', title: 'ELECTRICAL STORM', message: 'Massive discharge disrupting comms', color: '#ffff00', icon: '⛈️', recovery: 'Grounding excess charge...', recoverable: true, recoveryTime: [15, 45], recoveryChance: 0.94 }, { id: 'aurora', category: 'atmospheric', title: 'AURORAL INTERFERENCE', message: 'Charged particles in ionosphere', color: '#44ffaa', icon: '🌌', recovery: 'Tunneling through aurora belt...', recoverable: true, recoveryTime: [30, 90], recoveryChance: 0.91 }, // Biological/Alien - Recoverable { id: 'biofilm', category: 'biological', title: 'BIOFILM ACCUMULATION', message: 'Organic growth on sensor array', color: '#44aa44', icon: '🦠', recovery: 'UV sterilization active...', recoverable: true, recoveryTime: [45, 120], recoveryChance: 0.85 }, { id: 'spore_cloud', category: 'biological', title: 'SPORE CLOUD EVENT', message: 'Fungal spores blocking optics', color: '#886644', icon: '🍄', recovery: 'Air filtration cycling...', recoverable: true, recoveryTime: [30, 90], recoveryChance: 0.88 }, { id: 'bioluminescent_interference', category: 'biological', title: 'BIOLUMINESCENT BLOOM', message: 'Native lifeforms saturating sensors', color: '#00ffaa', icon: '✨', recovery: 'Adjusting light filters...', recoverable: true, recoveryTime: [20, 60], recoveryChance: 0.95 }, // Catastrophic Events - PERMANENT LOSS (recoverable: false) { id: 'planet_destroyed', category: 'catastrophic', title: 'PLANET DESTROYED', message: 'Catastrophic tectonic event detected', color: '#ff0000', icon: '💥', recovery: 'SIGNAL LOST PERMANENTLY', recoverable: false, lossReason: 'The planetary body has been completely destroyed. No surface remains for signal acquisition. Robot telemetry terminated at moment of cataclysm.' }, { id: 'star_death', category: 'catastrophic', title: 'STELLAR COLLAPSE', message: 'Primary star entering nova phase', color: '#ff4400', icon: '🌟', recovery: 'SIGNAL LOST PERMANENTLY', recoverable: false, lossReason: 'Supernova shockwave has vaporized all matter within the habitable zone. Quantum entanglement link severed by extreme radiation.' }, { id: 'wormhole_instability', category: 'catastrophic', title: 'WORMHOLE COLLAPSE', message: 'Transit corridor destabilizing', color: '#aa00ff', icon: '🌀', recovery: 'SIGNAL LOST PERMANENTLY', recoverable: false, lossReason: 'The wormhole terminus has collapsed. Robot is now in an unreachable region of spacetime. No known method of re-establishing contact.' }, { id: 'dimension_rift', category: 'catastrophic', title: 'DIMENSIONAL RIFT', message: 'Reality breach detected nearby', color: '#ff00aa', icon: '🔮', recovery: 'SIGNAL LOST PERMANENTLY', recoverable: false, lossReason: 'Robot has been pulled into an alternate dimension. Quantum signature is no longer detectable in our reality plane.' }, // Mundane/Relatable - Quick recovery { id: 'software_crash', category: 'mundane', title: 'SOFTWARE EXCEPTION', message: 'Unexpected null pointer reference', color: '#ff6666', icon: '🐛', recovery: 'Rebooting comm module...', recoverable: true, recoveryTime: [5, 15], recoveryChance: 0.99 }, { id: 'cable_loose', category: 'mundane', title: 'CONNECTION LOOSE', message: 'Physical connector vibrated free', color: '#ffaa44', icon: '🔌', recovery: 'Servo tightening connection...', recoverable: true, recoveryTime: [8, 20], recoveryChance: 0.98 }, { id: 'bird_equivalent', category: 'mundane', title: 'AVIAN INTERFERENCE', message: 'Local fauna perched on antenna', color: '#88aaff', icon: '🐦', recovery: 'Activating deterrent...', recoverable: true, recoveryTime: [5, 15], recoveryChance: 0.99 }, { id: 'coffee_spill', category: 'mundane', title: 'FLUID INTRUSION', message: 'Liquid detected in circuit bay', color: '#8b4513', icon: '☕', recovery: 'Engaging drying protocol...', recoverable: true, recoveryTime: [30, 90], recoveryChance: 0.80 }, // v6.3.5: Page Refresh/Session Events - Very quick recovery (user-initiated reconnection) { id: 'page_refresh', category: 'refresh', title: 'CONNECTION RESET', message: 'User initiated session restart', color: '#00aaff', icon: '🔄', recovery: 'Re-establishing uplink...', recoverable: true, recoveryTime: [3, 10], recoveryChance: 0.99 }, { id: 'session_restart', category: 'refresh', title: 'SESSION RESTART', message: 'Communication session reinitialized', color: '#44aaff', icon: '🔁', recovery: 'Synchronizing state...', recoverable: true, recoveryTime: [4, 12], recoveryChance: 0.99 }, { id: 'link_renegotiation', category: 'refresh', title: 'LINK RENEGOTIATION', message: 'Secure channel renegotiating keys', color: '#6688ff', icon: '🔐', recovery: 'Handshake in progress...', recoverable: true, recoveryTime: [5, 15], recoveryChance: 0.98 }, { id: 'buffer_flush', category: 'refresh', title: 'BUFFER FLUSH', message: 'Clearing transmission buffers', color: '#88aaff', icon: '📤', recovery: 'Reinitializing stream...', recoverable: true, recoveryTime: [2, 8], recoveryChance: 0.99 }, { id: 'protocol_sync', category: 'refresh', title: 'PROTOCOL SYNC', message: 'Realigning communication protocol', color: '#55aadd', icon: '📡', recovery: 'Matching frequencies...', recoverable: true, recoveryTime: [3, 10], recoveryChance: 0.99 }, { id: 'heartbeat_missed', category: 'refresh', title: 'HEARTBEAT MISSED', message: 'Keep-alive signal interrupted', color: '#ffaa00', icon: '💓', recovery: 'Resending heartbeat...', recoverable: true, recoveryTime: [2, 6], recoveryChance: 0.99 }, { id: 'auth_refresh', category: 'refresh', title: 'AUTH REFRESH', message: 'Refreshing authentication tokens', color: '#88ff88', icon: '🔑', recovery: 'Validating credentials...', recoverable: true, recoveryTime: [3, 8], recoveryChance: 0.99 }, { id: 'quantum_resync', category: 'refresh', title: 'QUANTUM RESYNC', message: 'Re-entangling quantum relay', color: '#ff88ff', icon: '⚛️', recovery: 'Aligning qubits...', recoverable: true, recoveryTime: [5, 15], recoveryChance: 0.95 } ], // Track if page was refreshed pageWasRefreshed: false, refreshDetected: false, // Get or generate current session's interruption event getCurrentEvent(agentId = null) { const storageKey = agentId ? `levi_signal_event_${agentId}` : 'levi_signal_event_global'; let eventData = null; // Check if agent is permanently lost if (agentId && this.isAgentLost(agentId)) { return this.getLostAgentEvent(agentId); } // Check if agent has recovered if (agentId && this.recoveredAgents.has(agentId)) { return { recovered: true, ...this.recoveredAgents.get(agentId) }; } try { const stored = sessionStorage.getItem(storageKey); if (stored) { eventData = JSON.parse(stored); // Verify it's still valid if (eventData && eventData.id && this.events.find(e => e.id === eventData.id)) { // Check if recovery timer should be started if (eventData.recoverable && !eventData.recoveryStarted && agentId) { this.startRecoveryTimer(agentId, eventData); eventData.recoveryStarted = true; sessionStorage.setItem(storageKey, JSON.stringify(eventData)); } // Update time remaining if (eventData.recoveryEndTime) { eventData.timeRemaining = Math.max(0, Math.ceil((eventData.recoveryEndTime - Date.now()) / 1000)); } return eventData; } } } catch (e) {} // Generate new event with weighted probability eventData = this.generateEvent(); // Add recovery timing data if (eventData.recoverable) { const [minTime, maxTime] = eventData.recoveryTime; const recoveryDuration = (minTime + Math.random() * (maxTime - minTime)) * 1000; eventData.recoveryEndTime = Date.now() + recoveryDuration; eventData.recoveryStarted = false; eventData.timeRemaining = Math.ceil(recoveryDuration / 1000); } try { sessionStorage.setItem(storageKey, JSON.stringify(eventData)); } catch (e) {} // If catastrophic and agent specified, mark as permanently lost if (!eventData.recoverable && agentId) { this.markAgentLost(agentId, eventData); } return eventData; }, // Start the recovery countdown timer for an agent startRecoveryTimer(agentId, eventData) { if (this.recoveryTimers.has(agentId)) return; const timeRemaining = eventData.recoveryEndTime - Date.now(); if (timeRemaining <= 0) { this.attemptRecovery(agentId, eventData); return; } const timer = setTimeout(() => { this.attemptRecovery(agentId, eventData); }, timeRemaining); this.recoveryTimers.set(agentId, timer); }, // Attempt to recover the signal attemptRecovery(agentId, eventData) { this.recoveryTimers.delete(agentId); // Roll for recovery success if (Math.random() < eventData.recoveryChance) { // Success! Signal recovered this.recoveredAgents.set(agentId, { id: 'signal_restored', originalEvent: eventData, recoveredAt: Date.now(), title: 'SIGNAL RESTORED', message: `Connection re-established after ${eventData.title.toLowerCase()}`, color: '#00ff88', icon: '📶' }); // Clear the stored event this.clearEvent(agentId); // Notify the game this.notifyRecovery(agentId, eventData); } else { // Failed recovery - extend timer and try again const extendTime = 15000 + Math.random() * 30000; // 15-45 more seconds eventData.recoveryEndTime = Date.now() + extendTime; eventData.timeRemaining = Math.ceil(extendTime / 1000); eventData.recoveryAttempts = (eventData.recoveryAttempts || 0) + 1; // Reduce chance slightly each failure (but minimum 30%) eventData.recoveryChance = Math.max(0.30, eventData.recoveryChance - 0.05); try { const storageKey = `levi_signal_event_${agentId}`; sessionStorage.setItem(storageKey, JSON.stringify(eventData)); } catch (e) {} // Schedule another attempt const timer = setTimeout(() => { this.attemptRecovery(agentId, eventData); }, extendTime); this.recoveryTimers.set(agentId, timer); // Notify of failed attempt if (typeof addCopilotMessage === 'function') { addCopilotMessage(`Recovery attempt ${eventData.recoveryAttempts} failed for agent. Retrying...`, 'system'); } } }, // Notify the game that an agent's signal has been restored notifyRecovery(agentId, originalEvent) { // Find the agent if (typeof agentFleet !== 'undefined') { const agent = agentLookup.get(agentId); if (agent) { // Show recovery notification if (typeof addCopilotMessage === 'function') { addCopilotMessage(`📶 SIGNAL RESTORED: ${agent.name} back online after ${originalEvent.title.toLowerCase()}. Ready to reconnect!`, 'ai'); } // Show toast notification if (typeof showToast === 'function') { showToast(`${agent.name} signal restored! Click to reconnect.`, 'success'); } // Create reconnect prompt this.showReconnectPrompt(agent, originalEvent); } } }, // Show UI prompt to reconnect to recovered agent showReconnectPrompt(agent, originalEvent) { // Create floating reconnect button const existingPrompt = document.getElementById(`reconnect-prompt-${agent.id}`); if (existingPrompt) existingPrompt.remove(); const prompt = document.createElement('div'); prompt.id = `reconnect-prompt-${agent.id}`; prompt.className = 'signal-reconnect-prompt'; prompt.innerHTML = `
📶 SIGNAL RESTORED
${agent.name}
Recovered from: ${originalEvent.title}
`; // Add styles if not present if (!document.getElementById('signal-reconnect-styles')) { const styles = document.createElement('style'); styles.id = 'signal-reconnect-styles'; styles.textContent = ` .signal-reconnect-prompt { position: fixed; bottom: 100px; right: 20px; background: linear-gradient(135deg, rgba(0, 40, 30, 0.95), rgba(0, 20, 15, 0.98)); border: 2px solid #00ff88; border-radius: 12px; padding: 16px; z-index: 9999; animation: signalPulse 2s ease-in-out infinite; box-shadow: 0 0 30px rgba(0, 255, 136, 0.3); min-width: 250px; } @keyframes signalPulse { 0%, 100% { box-shadow: 0 0 20px rgba(0, 255, 136, 0.3); } 50% { box-shadow: 0 0 40px rgba(0, 255, 136, 0.6); } } .reconnect-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } .reconnect-icon { font-size: 20px; animation: iconBounce 1s ease-in-out infinite; } @keyframes iconBounce { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-3px); } } .reconnect-title { color: #00ff88; font-weight: bold; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; } .reconnect-agent { color: #fff; font-size: 16px; font-weight: bold; margin-bottom: 4px; } .reconnect-event { color: #aaa; font-size: 11px; margin-bottom: 12px; } .reconnect-btn { width: 100%; padding: 10px; background: linear-gradient(135deg, #00aa66, #008844); border: none; border-radius: 6px; color: #fff; font-weight: bold; cursor: pointer; transition: all 0.2s; } .reconnect-btn:hover { background: linear-gradient(135deg, #00cc77, #00aa55); transform: scale(1.02); } .reconnect-dismiss { position: absolute; top: 8px; right: 8px; background: none; border: none; color: #999; cursor: pointer; font-size: 14px; } .reconnect-dismiss:hover { color: #fff; } `; document.head.appendChild(styles); } document.body.appendChild(prompt); // Auto-dismiss after 60 seconds setTimeout(() => { if (document.getElementById(`reconnect-prompt-${agent.id}`)) { prompt.remove(); } }, 60000); }, // Reconnect to an agent after signal restoration reconnectToAgent(agentId) { const recoveryData = this.recoveredAgents.get(agentId); if (!recoveryData) return; // Clear recovery data this.recoveredAgents.delete(agentId); // Remove prompt const prompt = document.getElementById(`reconnect-prompt-${agentId}`); if (prompt) prompt.remove(); // Find agent and teleport player to their location if (typeof agentFleet !== 'undefined' && typeof playerMesh !== 'undefined') { const agent = agentLookup.get(agentId); if (agent && agent.mesh) { // Teleport player to agent location const agentPos = agent.mesh.position; playerMesh.position.set(agentPos.x, agentPos.y + 2, agentPos.z + 5); if (typeof addCopilotMessage === 'function') { addCopilotMessage(`🚀 Teleported to ${agent.name}'s location. Signal lock confirmed!`, 'ai'); } if (typeof showToast === 'function') { showToast(`Reconnected to ${agent.name}!`, 'success'); } } } }, // Mark an agent as permanently lost markAgentLost(agentId, eventData) { const lossData = { agentId, event: eventData, lostAt: Date.now(), lastKnownPosition: null }; // Try to capture last known position if (typeof agentFleet !== 'undefined') { const agent = agentLookup.get(agentId); if (agent) { lossData.lastKnownPosition = agent.mesh ? { x: agent.mesh.position.x, y: agent.mesh.position.y, z: agent.mesh.position.z } : agent.position; lossData.agentName = agent.name; } } this.lostAgents.add(agentId); // Store in localStorage for persistence across sessions // v8.0: Now using SafeJSON utility (8-Strategy Consensus Cycle 1) const lostAgentsData = SafeJSON.fromLocalStorage('levi_lost_agents', {}); lostAgentsData[agentId] = lossData; SafeJSON.toLocalStorage('levi_lost_agents', lostAgentsData); // Notify the player if (typeof addCopilotMessage === 'function') { addCopilotMessage(`⚠️ CATASTROPHIC SIGNAL LOSS: ${lossData.agentName || 'Agent'} has been permanently lost due to ${eventData.title}. ${eventData.lossReason}`, 'error'); } // Show memorial notification this.showLossNotification(lossData, eventData); }, // Check if an agent is permanently lost // v8.0: Now using SafeJSON utility (8-Strategy Consensus Cycle 1) isAgentLost(agentId) { if (this.lostAgents.has(agentId)) return true; const lostAgentsData = SafeJSON.fromLocalStorage('levi_lost_agents', {}); if (lostAgentsData[agentId]) { this.lostAgents.add(agentId); return true; } return false; }, // Get the loss event for a lost agent // v8.0: Now using SafeJSON utility (8-Strategy Consensus Cycle 1) getLostAgentEvent(agentId) { const lostAgentsData = SafeJSON.fromLocalStorage('levi_lost_agents', {}); if (lostAgentsData[agentId]) { const lossData = lostAgentsData[agentId]; return { ...lossData.event, permanentlyLost: true, lostAt: lossData.lostAt, lastKnownPosition: lossData.lastKnownPosition, timeSinceLoss: Date.now() - lossData.lostAt }; } return { id: 'unknown_loss', category: 'catastrophic', title: 'SIGNAL LOST', message: 'Connection permanently severed', color: '#ff0000', icon: '💀', recovery: 'NO RECOVERY POSSIBLE', permanentlyLost: true }; }, // Show notification for permanent agent loss showLossNotification(lossData, eventData) { const notification = document.createElement('div'); notification.className = 'signal-loss-notification'; notification.innerHTML = `
${eventData.icon}
${eventData.title}
${lossData.agentName || 'AGENT'} LOST
${eventData.message}
${eventData.lossReason}
In memory of a faithful explorer
`; // Add styles if not present if (!document.getElementById('signal-loss-styles')) { const styles = document.createElement('style'); styles.id = 'signal-loss-styles'; styles.textContent = ` .signal-loss-notification { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: linear-gradient(135deg, rgba(40, 0, 0, 0.98), rgba(20, 0, 0, 0.99)); border: 3px solid #ff0000; border-radius: 16px; padding: 30px; z-index: 99999; text-align: center; animation: lossAppear 0.5s ease-out; box-shadow: 0 0 60px rgba(255, 0, 0, 0.5), inset 0 0 60px rgba(255, 0, 0, 0.1); max-width: 400px; } @keyframes lossAppear { from { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } .loss-icon { font-size: 48px; margin-bottom: 16px; animation: lossIconPulse 1s ease-in-out infinite; } @keyframes lossIconPulse { 0%, 100% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.1); filter: brightness(1.5); } } .loss-title { color: #ff4444; font-size: 24px; font-weight: bold; text-transform: uppercase; letter-spacing: 3px; margin-bottom: 8px; text-shadow: 0 0 20px rgba(255, 0, 0, 0.8); } .loss-agent { color: #fff; font-size: 18px; font-weight: bold; margin-bottom: 16px; } .loss-message { color: #ff8888; font-size: 14px; margin-bottom: 12px; } .loss-reason { color: #aaa; font-size: 12px; line-height: 1.5; margin-bottom: 20px; padding: 12px; background: rgba(0,0,0,0.3); border-radius: 8px; } .loss-memorial { border-top: 1px solid #440000; padding-top: 16px; margin-bottom: 20px; } .memorial-text { color: #999; font-style: italic; font-size: 13px; } .loss-dismiss { padding: 12px 30px; background: linear-gradient(135deg, #880000, #660000); border: 1px solid #aa0000; border-radius: 6px; color: #fff; font-weight: bold; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: all 0.2s; } .loss-dismiss:hover { background: linear-gradient(135deg, #aa0000, #880000); } `; document.head.appendChild(styles); } document.body.appendChild(notification); }, // Generate a weighted random event generateEvent(forceCategory = null) { // Weight categories (mundane/environmental more common than catastrophic) // v6.3.5: Added refresh category (only used when page refresh detected) const weights = { environmental: 30, technical: 25, atmospheric: 20, celestial: 10, biological: 8, mundane: 5, catastrophic: 2, refresh: 0 // Refresh events are only assigned explicitly, not randomly }; // v6.3.5: If forcing a category, only use events from that category if (forceCategory) { const categoryEvents = this.events.filter(e => e.category === forceCategory); if (categoryEvents.length > 0) { return { ...categoryEvents[Math.floor(Math.random() * categoryEvents.length)] }; } } // Build weighted array const weightedEvents = []; this.events.forEach(event => { const weight = weights[event.category] || 10; for (let i = 0; i < weight; i++) { weightedEvents.push(event); } }); // Return a copy with the base event data const selected = weightedEvents[Math.floor(Math.random() * weightedEvents.length)]; return { ...selected }; }, // Clear event (for when signal is restored) clearEvent(agentId = null) { const storageKey = agentId ? `levi_signal_event_${agentId}` : 'levi_signal_event_global'; try { sessionStorage.removeItem(storageKey); } catch (e) {} // Clear any pending timer if (agentId && this.recoveryTimers.has(agentId)) { clearTimeout(this.recoveryTimers.get(agentId)); this.recoveryTimers.delete(agentId); } }, // Get event-specific visual noise parameters getNoiseParams(event) { const params = { density: 0.05, intensity: 30, colorTint: null, scanlines: false, glitch: false }; switch (event.category) { case 'environmental': params.density = 0.08; params.colorTint = event.color; params.scanlines = true; break; case 'technical': params.density = 0.03; params.glitch = true; break; case 'celestial': params.density = 0.02; params.intensity = 15; break; case 'atmospheric': params.density = 0.12; params.colorTint = event.color; break; case 'biological': params.density = 0.06; params.colorTint = event.color; break; case 'catastrophic': params.density = 0.15; params.intensity = 60; params.glitch = true; params.scanlines = true; break; case 'mundane': params.density = 0.04; break; case 'refresh': // v6.3.5: Refresh events - clean digital reconnection look params.density = 0.02; params.intensity = 20; params.colorTint = event.color; params.scanlines = false; params.glitch = false; break; } return params; }, // Format time remaining for display formatTimeRemaining(seconds) { if (seconds <= 0) return 'Imminent...'; if (seconds < 60) return `${seconds}s`; const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}m ${secs}s`; }, // ============================================ // v6.3.4: EXPORT/IMPORT STATE SYSTEM // Complete backup and restore of signal state // ============================================ // Export complete signal interruption state exportState() { const state = { version: '1.0', exportType: 'SignalInterruptionSystem', exportDate: new Date().toISOString(), exportTimestamp: Date.now(), // Permanently lost agents from localStorage lostAgents: {}, // Active signal events from sessionStorage (per-agent) activeEvents: {}, // Recovered agents waiting for reconnection recoveredAgents: {}, // In-memory lost agents set (for verification) lostAgentIds: [], // Metadata for verification metadata: { totalLostAgents: 0, totalActiveEvents: 0, totalRecoveredAgents: 0, agentNames: {} } }; // Export permanently lost agents from localStorage try { const lostData = localStorage.getItem('levi_lost_agents'); if (lostData) { // v8.29: Use ErrorRecovery.safeJSONParse for safer parsing state.lostAgents = ErrorRecovery.safeJSONParse(lostData, {}); state.metadata.totalLostAgents = Object.keys(state.lostAgents).length; // Capture agent names Object.entries(state.lostAgents).forEach(([id, data]) => { if (data.agentName) { state.metadata.agentNames[id] = data.agentName; } }); } } catch (e) { console.error('Failed to export lost agents:', e); } // Export in-memory lost agent IDs state.lostAgentIds = Array.from(this.lostAgents); // Export active signal events from sessionStorage try { // Get all sessionStorage keys that match our pattern for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); if (key && key.startsWith('levi_signal_event_')) { const eventData = sessionStorage.getItem(key); if (eventData) { const agentId = key.replace('levi_signal_event_', ''); // v8.29: Use ErrorRecovery.safeJSONParse for safer parsing const parsed = ErrorRecovery.safeJSONParse(eventData, null); if (parsed) { state.activeEvents[agentId] = parsed; state.metadata.totalActiveEvents++; } // Try to get agent name if (typeof agentFleet !== 'undefined') { const agent = agentLookup.get(agentId); if (agent && agent.name) { state.metadata.agentNames[agentId] = agent.name; } } } } } } catch (e) { console.error('Failed to export active events:', e); } // Export recovered agents from memory this.recoveredAgents.forEach((data, agentId) => { state.recoveredAgents[agentId] = data; state.metadata.totalRecoveredAgents++; // Try to get agent name if (typeof agentFleet !== 'undefined') { const agent = agentLookup.get(agentId); if (agent && agent.name) { state.metadata.agentNames[agentId] = agent.name; } } }); return state; }, // Import and restore signal interruption state importState(state, options = {}) { const { mergeLostAgents = true, // Add to existing lost agents vs replace restoreActiveEvents = true, // Restore active signal events restoreRecovered = true, // Restore recovered agents adjustTimestamps = true // Adjust timestamps relative to current time } = options; if (!state || state.exportType !== 'SignalInterruptionSystem') { throw new Error('Invalid signal state backup format'); } const results = { lostAgentsRestored: 0, activeEventsRestored: 0, recoveredAgentsRestored: 0, errors: [] }; // Calculate time offset for timestamp adjustment const timeOffset = adjustTimestamps ? (Date.now() - state.exportTimestamp) : 0; // Restore permanently lost agents to localStorage if (state.lostAgents && Object.keys(state.lostAgents).length > 0) { try { let existingLost = {}; if (mergeLostAgents) { const existing = localStorage.getItem('levi_lost_agents'); if (existing) { // v8.29: Use ErrorRecovery.safeJSONParse for safer parsing existingLost = ErrorRecovery.safeJSONParse(existing, {}); } } // Merge or replace lost agents Object.entries(state.lostAgents).forEach(([agentId, lossData]) => { // Adjust timestamp if needed if (adjustTimestamps && lossData.lostAt) { lossData.lostAt = lossData.lostAt + timeOffset; } existingLost[agentId] = lossData; this.lostAgents.add(agentId); results.lostAgentsRestored++; }); localStorage.setItem('levi_lost_agents', JSON.stringify(existingLost)); } catch (e) { results.errors.push(`Failed to restore lost agents: ${e.message}`); } } // Restore lost agent IDs to in-memory set if (state.lostAgentIds && Array.isArray(state.lostAgentIds)) { state.lostAgentIds.forEach(id => this.lostAgents.add(id)); } // Restore active signal events to sessionStorage if (restoreActiveEvents && state.activeEvents) { Object.entries(state.activeEvents).forEach(([agentId, eventData]) => { try { // Skip if agent is permanently lost if (this.lostAgents.has(agentId)) { return; } // Adjust recovery timestamps if (adjustTimestamps && eventData.recoveryEndTime) { eventData.recoveryEndTime = eventData.recoveryEndTime + timeOffset; eventData.timeRemaining = Math.max(0, Math.ceil((eventData.recoveryEndTime - Date.now()) / 1000)); } // If event already expired, mark for immediate recovery attempt if (eventData.recoveryEndTime && eventData.recoveryEndTime < Date.now()) { eventData.timeRemaining = 0; } const storageKey = `levi_signal_event_${agentId}`; sessionStorage.setItem(storageKey, JSON.stringify(eventData)); // Restart recovery timer if applicable if (eventData.recoverable && eventData.recoveryEndTime) { eventData.recoveryStarted = false; // Reset so timer gets started this.startRecoveryTimer(agentId, eventData); } results.activeEventsRestored++; } catch (e) { results.errors.push(`Failed to restore event for ${agentId}: ${e.message}`); } }); } // Restore recovered agents to memory if (restoreRecovered && state.recoveredAgents) { Object.entries(state.recoveredAgents).forEach(([agentId, recoveryData]) => { try { // Skip if agent is permanently lost if (this.lostAgents.has(agentId)) { return; } // Adjust recovery timestamp if (adjustTimestamps && recoveryData.recoveredAt) { recoveryData.recoveredAt = recoveryData.recoveredAt + timeOffset; } this.recoveredAgents.set(agentId, recoveryData); // Show reconnect prompt for recovered agents if (typeof agentFleet !== 'undefined') { const agent = agentLookup.get(agentId); if (agent) { this.showReconnectPrompt(agent, recoveryData.originalEvent || { title: 'Previous Event' }); } } results.recoveredAgentsRestored++; } catch (e) { results.errors.push(`Failed to restore recovered agent ${agentId}: ${e.message}`); } }); } return results; }, // Download state as JSON file downloadStateBackup() { const state = this.exportState(); const dataStr = JSON.stringify(state, null, 2); const dataBlob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(dataBlob); const link = document.createElement('a'); link.href = url; const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); link.download = `leviathan-signal-state-${timestamp}.json`; link.click(); URL.revokeObjectURL(url); // Show notification if (typeof showToast === 'function') { showToast('Signal state backup downloaded!', 'success'); } if (typeof addCopilotMessage === 'function') { addCopilotMessage(`📡 Signal state exported: ${state.metadata.totalLostAgents} lost agents, ${state.metadata.totalActiveEvents} active events, ${state.metadata.totalRecoveredAgents} recovered agents.`, 'system'); } return state; }, // Load state from file picker loadStateFromFile() { return new Promise((resolve, reject) => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) { reject(new Error('No file selected')); return; } try { const text = await file.text(); // v8.29: Use ErrorRecovery.safeJSONParse for safer parsing const state = ErrorRecovery.safeJSONParse(text, null); if (!state) { throw new Error('Failed to parse backup file'); } // Validate format if (!state.exportType || state.exportType !== 'SignalInterruptionSystem') { // Check if it's a full backup that contains signal state if (state.signalInterruptionState) { const results = this.importState(state.signalInterruptionState); resolve(results); return; } throw new Error('Invalid signal state backup format. Expected SignalInterruptionSystem export.'); } const results = this.importState(state); // Show notification if (typeof showToast === 'function') { if (results.errors.length > 0) { showToast(`Signal state restored with ${results.errors.length} warnings`, 'warning'); } else { showToast('Signal state restored successfully!', 'success'); } } if (typeof addCopilotMessage === 'function') { addCopilotMessage(`📡 Signal state imported: ${results.lostAgentsRestored} lost agents, ${results.activeEventsRestored} active events, ${results.recoveredAgentsRestored} recovered agents restored.`, 'ai'); } resolve(results); } catch (err) { if (typeof showToast === 'function') { showToast(`Failed to load signal state: ${err.message}`, 'error'); } reject(err); } }; input.click(); }); }, // Show backup/restore UI modal showBackupRestoreUI() { // Remove existing modal if present const existing = document.getElementById('signal-backup-modal'); if (existing) existing.remove(); const state = this.exportState(); const modal = document.createElement('div'); modal.id = 'signal-backup-modal'; // v7.78: Added ARIA attributes for accessibility modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-labelledby', 'signal-backup-title'); modal.innerHTML = `

📡 Signal State Backup

${state.metadata.totalLostAgents} Permanently Lost
${state.metadata.totalActiveEvents} Active Events
${state.metadata.totalRecoveredAgents} Awaiting Reconnect
${Object.keys(state.metadata.agentNames).length > 0 ? `

Affected Agents

${Object.entries(state.metadata.agentNames).map(([id, name]) => { const isLost = state.lostAgents[id]; const isRecovered = state.recoveredAgents[id]; const isActive = state.activeEvents[id]; const statusIcon = isLost ? '💀' : (isRecovered ? '📶' : '⏳'); const statusClass = isLost ? 'lost' : (isRecovered ? 'recovered' : 'active'); return `${statusIcon} ${name}`; }).join('')}
` : ''}

⚠️ Danger Zone

💡 Export your signal state before closing the browser to preserve:

  • Permanently lost robots and their memorial data
  • Active signal interruption events with recovery progress
  • Robots waiting for reconnection
`; // Add styles if (!document.getElementById('signal-backup-styles')) { const styles = document.createElement('style'); styles.id = 'signal-backup-styles'; styles.textContent = ` #signal-backup-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 100000; display: flex; align-items: center; justify-content: center; } .signal-backup-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); backdrop-filter: blur(5px); } .signal-backup-content { position: relative; background: linear-gradient(135deg, rgba(15, 25, 35, 0.98), rgba(10, 15, 25, 0.99)); border: 2px solid #0af; border-radius: 16px; padding: 30px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: 0 0 60px rgba(0, 170, 255, 0.3); animation: modalAppear 0.3s ease-out; } @keyframes modalAppear { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } .signal-backup-close { position: absolute; top: 15px; right: 15px; background: none; border: none; color: #aaa; font-size: 20px; cursor: pointer; transition: color 0.2s; } .signal-backup-close:hover { color: #fff; } .signal-backup-title { margin: 0 0 20px 0; color: #0af; font-size: 22px; text-align: center; } .signal-backup-stats { display: flex; gap: 15px; margin-bottom: 20px; } .stat-card { flex: 1; background: rgba(0, 0, 0, 0.3); border-radius: 10px; padding: 15px; text-align: center; } .stat-value { display: block; font-size: 28px; font-weight: bold; color: #fff; } .stat-label { display: block; font-size: 11px; color: #aaa; margin-top: 5px; } .signal-backup-agents { margin-bottom: 20px; } .signal-backup-agents h3 { color: #aaa; font-size: 13px; margin: 0 0 10px 0; } .agent-list { display: flex; flex-wrap: wrap; gap: 8px; } .agent-tag { padding: 5px 10px; border-radius: 15px; font-size: 12px; background: rgba(0, 0, 0, 0.3); } .agent-tag.lost { border: 1px solid #ff4444; color: #ff8888; } .agent-tag.recovered { border: 1px solid #00ff88; color: #aaffaa; } .agent-tag.active { border: 1px solid #ffaa00; color: #ffcc66; } .signal-backup-actions { display: flex; gap: 15px; margin-bottom: 20px; } .backup-btn { flex: 1; padding: 15px; border: none; border-radius: 10px; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; align-items: center; gap: 5px; } .backup-btn.export { background: linear-gradient(135deg, #0077aa, #005588); color: #fff; } .backup-btn.export:hover { background: linear-gradient(135deg, #0099cc, #0077aa); transform: translateY(-2px); } .backup-btn.import { background: linear-gradient(135deg, #007744, #005533); color: #fff; } .backup-btn.import:hover { background: linear-gradient(135deg, #009966, #007744); transform: translateY(-2px); } .backup-btn.danger { background: linear-gradient(135deg, #773333, #552222); color: #ffaaaa; flex: none; width: 48%; } .backup-btn.danger:hover { background: linear-gradient(135deg, #994444, #773333); } .btn-icon { font-size: 24px; } .btn-text { font-size: 13px; font-weight: bold; } .signal-backup-danger { margin-bottom: 20px; padding: 15px; background: rgba(100, 30, 30, 0.2); border: 1px solid #662222; border-radius: 10px; } .signal-backup-danger h3 { color: #ff6666; font-size: 13px; margin: 0 0 10px 0; } .signal-backup-danger .backup-btn { display: inline-flex; flex-direction: row; padding: 10px 15px; margin-right: 10px; } .signal-backup-danger .btn-icon { font-size: 16px; margin-right: 8px; } .signal-backup-info { background: rgba(0, 100, 150, 0.1); border: 1px solid #0af; border-radius: 10px; padding: 15px; } .signal-backup-info p { color: #0af; margin: 0 0 10px 0; font-size: 12px; } .signal-backup-info ul { margin: 0; padding-left: 20px; color: #aaa; font-size: 11px; } .signal-backup-info li { margin-bottom: 5px; } `; document.head.appendChild(styles); } document.body.appendChild(modal); }, // Clear all lost agents (danger zone) clearAllLostAgents() { try { localStorage.removeItem('levi_lost_agents'); this.lostAgents.clear(); if (typeof showToast === 'function') { showToast('All lost agents cleared', 'success'); } if (typeof addCopilotMessage === 'function') { addCopilotMessage('🔄 All permanently lost agents have been cleared. Memorial data erased.', 'system'); } } catch (e) { console.error('Failed to clear lost agents:', e); } }, // Reset all signal state (danger zone) resetAllState() { try { // Clear localStorage localStorage.removeItem('levi_lost_agents'); // Clear sessionStorage signal events const keysToRemove = []; for (let i = 0; i < sessionStorage.length; i++) { const key = sessionStorage.key(i); if (key && key.startsWith('levi_signal_event_')) { keysToRemove.push(key); } } keysToRemove.forEach(key => sessionStorage.removeItem(key)); // Clear in-memory state this.recoveryTimers.forEach(timer => clearTimeout(timer)); this.recoveryTimers.clear(); this.recoveredAgents.clear(); this.lostAgents.clear(); if (typeof showToast === 'function') { showToast('All signal state reset', 'success'); } if (typeof addCopilotMessage === 'function') { addCopilotMessage('🔄 Signal Interruption System fully reset. All events, lost agents, and recovery data cleared.', 'system'); } } catch (e) { console.error('Failed to reset signal state:', e); } }, // Get state summary for integration with main game backup getStateSummary() { const state = this.exportState(); return { hasData: state.metadata.totalLostAgents > 0 || state.metadata.totalActiveEvents > 0 || state.metadata.totalRecoveredAgents > 0, lostCount: state.metadata.totalLostAgents, activeCount: state.metadata.totalActiveEvents, recoveredCount: state.metadata.totalRecoveredAgents, agentNames: state.metadata.agentNames }; }, // ============================================ // v6.3.5: PAGE REFRESH DETECTION SYSTEM // Detects page refresh and assigns temporary // signal loss events to all active agents // ============================================ // Detect if the page was refreshed (vs fresh navigation) detectPageRefresh() { // Method 1: Use Performance Navigation API (modern browsers) if (window.performance && window.performance.getEntriesByType) { const navEntries = performance.getEntriesByType('navigation'); if (navEntries.length > 0 && navEntries[0].type === 'reload') { return true; } } // Method 2: Use deprecated but widely supported navigation.type if (window.performance && window.performance.navigation) { if (performance.navigation.type === 1) { // TYPE_RELOAD return true; } } // Method 3: Check sessionStorage flag (set before unload) try { const wasHere = sessionStorage.getItem('levi_session_active'); if (wasHere === 'true') { // We were here before - this is a refresh or back navigation return true; } } catch (e) {} return false; }, // Initialize refresh detection on page load initRefreshDetection() { // Check if this is a refresh this.pageWasRefreshed = this.detectPageRefresh(); this.refreshDetected = this.pageWasRefreshed; // Set flag for future refresh detection try { sessionStorage.setItem('levi_session_active', 'true'); } catch (e) {} // Set up beforeunload to track that we're leaving window.addEventListener('beforeunload', () => { try { // Mark the timestamp of when we left sessionStorage.setItem('levi_last_active', Date.now().toString()); } catch (e) {} }); if (this.pageWasRefreshed) { console.log('[SignalInterruptionSystem] Page refresh detected - will assign refresh events to agents'); } return this.pageWasRefreshed; }, // Handle page refresh by assigning refresh events to all agents // Call this after agents are loaded/initialized handlePageRefresh(agents = null) { if (!this.pageWasRefreshed) { return { handled: false, reason: 'Not a page refresh' }; } // Only handle refresh once per session if (this.refreshHandled) { return { handled: false, reason: 'Already handled this refresh' }; } this.refreshHandled = true; // Get agents from global agentFleet if not provided const agentList = agents || (typeof agentFleet !== 'undefined' ? agentFleet : []); if (!agentList || agentList.length === 0) { return { handled: false, reason: 'No agents available' }; } const results = { handled: true, agentsAffected: 0, agentsSkipped: 0, events: [] }; // v8.16: forEach-to-for optimization for (let ai = 0, alen = agentList.length; ai < alen; ai++) { const agent = agentList[ai]; if (!agent || !agent.id) continue; // Skip if agent is permanently lost if (this.isAgentLost(agent.id)) { results.agentsSkipped++; continue; } // Skip if agent already has an active event that's not a refresh event // (preserve existing events like sandstorms, etc.) const storageKey = `levi_signal_event_${agent.id}`; try { const existing = sessionStorage.getItem(storageKey); if (existing) { // v8.29: Use ErrorRecovery.safeJSONParse for safer parsing const existingEvent = ErrorRecovery.safeJSONParse(existing, null); if (!existingEvent) continue; // If it's a non-refresh event that hasn't recovered yet, keep it if (existingEvent.category !== 'refresh' && existingEvent.recoverable && existingEvent.recoveryEndTime > Date.now()) { results.agentsSkipped++; continue; } } } catch (e) {} // Generate a refresh event for this agent const refreshEvent = this.generateEvent('refresh'); // Set up recovery timing (very quick for refresh events) const [minTime, maxTime] = refreshEvent.recoveryTime; const recoveryDuration = (minTime + Math.random() * (maxTime - minTime)) * 1000; refreshEvent.recoveryEndTime = Date.now() + recoveryDuration; refreshEvent.recoveryStarted = false; refreshEvent.timeRemaining = Math.ceil(recoveryDuration / 1000); refreshEvent.isRefreshEvent = true; // Store the event try { sessionStorage.setItem(storageKey, JSON.stringify(refreshEvent)); } catch (e) {} // Clear any existing recovery state this.recoveredAgents.delete(agent.id); results.agentsAffected++; results.events.push({ agentId: agent.id, agentName: agent.name, event: refreshEvent }); } // Show notification if agents were affected if (results.agentsAffected > 0) { if (typeof addCopilotMessage === 'function') { addCopilotMessage(`🔄 Connection reset detected. Re-establishing signal with ${results.agentsAffected} agent${results.agentsAffected > 1 ? 's' : ''}...`, 'system'); } if (typeof showToast === 'function') { showToast(`Reconnecting to ${results.agentsAffected} agent${results.agentsAffected > 1 ? 's' : ''}...`, 'info'); } } return results; }, // Check if a specific agent has a refresh event pending hasRefreshEvent(agentId) { try { const storageKey = `levi_signal_event_${agentId}`; const stored = sessionStorage.getItem(storageKey); if (stored) { // v8.29: Use ErrorRecovery.safeJSONParse for safer parsing const event = ErrorRecovery.safeJSONParse(stored, null); return event && (event.category === 'refresh' || event.isRefreshEvent === true); } } catch (e) {} return false; }, // Clear refresh event for an agent (call when agent mesh is ready) clearRefreshEventIfReady(agentId, agentMeshReady = false) { if (!agentMeshReady) return false; try { const storageKey = `levi_signal_event_${agentId}`; const stored = sessionStorage.getItem(storageKey); if (stored) { const event = JSON.parse(stored); // Only auto-clear refresh events when mesh is ready if (event.category === 'refresh' || event.isRefreshEvent) { // Check if recovery time has passed if (event.recoveryEndTime && Date.now() >= event.recoveryEndTime) { // Signal restored! this.clearEvent(agentId); if (typeof agentFleet !== 'undefined') { const agent = agentLookup.get(agentId); if (agent) { if (typeof showToast === 'function') { showToast(`${agent.name} signal restored!`, 'success'); } } } return true; } } } } catch (e) {} return false; } }; // v6.3.5: Initialize refresh detection on script load SignalInterruptionSystem.initRefreshDetection(); // v6.3.4: Keyboard shortcut for signal backup UI (Ctrl+Shift+S) document.addEventListener('keydown', (e) => { if (e.ctrlKey && e.shiftKey && e.key === 'S') { e.preventDefault(); SignalInterruptionSystem.showBackupRestoreUI(); } }); // v5.16.3: Render placeholder when body cam can't display real view // v6.3.0: Enhanced to show agent is working even without visual // v6.3.2: Dynamic signal interruption events with unique visuals // v6.3.3: Recovery countdown, reconnection, and permanent loss display function renderBodyCamPlaceholder(agent, message = null) { if (!agent) return; const canvas = document.getElementById(`bodycam-${agent.id}`); if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // v6.3.2: Get or generate signal interruption event let signalEvent = null; // v6.3.0: Auto-generate message based on agent state if (!message) { if (agent.meshPending) { message = 'Loading 3D view...'; } else if (!scene) { message = 'World loading...'; } else { // v6.3.2: Use dynamic signal interruption event signalEvent = SignalInterruptionSystem.getCurrentEvent(agent.id); message = null; // Will be handled by event display } } // Dark background with noise effect ctx.fillStyle = '#0a0a0f'; ctx.fillRect(0, 0, canvas.width, canvas.height); // v6.3.2: Get event-specific noise parameters const noiseParams = signalEvent ? SignalInterruptionSystem.getNoiseParams(signalEvent) : { density: 0.05, intensity: 30 }; // v6.3.3: Special handling for recovered signals - green static if (signalEvent && signalEvent.recovered) { noiseParams.colorTint = '#00ff88'; noiseParams.density = 0.03; noiseParams.scanlines = false; noiseParams.glitch = false; } // v6.3.3: Special handling for permanent loss - heavy red static if (signalEvent && signalEvent.permanentlyLost) { noiseParams.colorTint = '#ff0000'; noiseParams.density = 0.20; noiseParams.intensity = 80; noiseParams.scanlines = true; noiseParams.glitch = true; } // v6.3.5: Special handling for refresh events - clean digital reconnection if (signalEvent && (signalEvent.category === 'refresh' || signalEvent.isRefreshEvent)) { noiseParams.colorTint = signalEvent.color || '#00aaff'; noiseParams.density = 0.015; noiseParams.intensity = 15; noiseParams.scanlines = false; noiseParams.glitch = false; } // Add static noise effect with event-specific parameters const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; for (let i = 0; i < data.length; i += 4) { if (Math.random() < noiseParams.density) { const noise = Math.random() * noiseParams.intensity; if (noiseParams.colorTint && Math.random() < 0.3) { // Parse hex color and apply tint const r = parseInt(noiseParams.colorTint.slice(1, 3), 16) || 0; const g = parseInt(noiseParams.colorTint.slice(3, 5), 16) || 0; const b = parseInt(noiseParams.colorTint.slice(5, 7), 16) || 0; data[i] = Math.min(255, noise + r * 0.3); data[i + 1] = Math.min(255, noise + g * 0.3); data[i + 2] = Math.min(255, noise + b * 0.3); } else { data[i] = noise; // R data[i + 1] = noise; // G data[i + 2] = noise; // B } } } ctx.putImageData(imageData, 0, 0); // v6.3.2: Add scanlines effect for certain events if (noiseParams.scanlines) { ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'; for (let y = 0; y < canvas.height; y += 3) { ctx.fillRect(0, y, canvas.width, 1); } } // v6.3.2: Add glitch effect for technical/catastrophic events if (noiseParams.glitch && Math.random() < 0.3) { const glitchY = Math.random() * canvas.height; const glitchH = 5 + Math.random() * 15; const glitchShift = (Math.random() - 0.5) * 20; const glitchData = ctx.getImageData(0, glitchY, canvas.width, glitchH); ctx.putImageData(glitchData, glitchShift, glitchY); } // v6.3.2: Draw signal event display OR simple message if (signalEvent) { ctx.textAlign = 'center'; // v6.3.3: Handle recovered signal state if (signalEvent.recovered) { // Recovered - show reconnect prompt ctx.font = '24px sans-serif'; ctx.fillText('📶', canvas.width/2, canvas.height/2 - 30); ctx.fillStyle = '#00ff88'; ctx.font = 'bold 12px monospace'; ctx.fillText('SIGNAL RESTORED', canvas.width/2, canvas.height/2 - 5); ctx.fillStyle = '#aaffaa'; ctx.font = '9px monospace'; ctx.fillText('Click to reconnect', canvas.width/2, canvas.height/2 + 12); // Pulsing border const pulse = (Math.sin(Date.now() / 300) + 1) / 2; ctx.strokeStyle = `rgba(0, 255, 136, ${0.5 + pulse * 0.5})`; ctx.lineWidth = 3; ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4); // Make canvas clickable for reconnection canvas.style.cursor = 'pointer'; canvas.onclick = () => SignalInterruptionSystem.reconnectToAgent(agent.id); return; // Don't show other info for recovered state } // v6.3.3: Handle permanently lost state if (signalEvent.permanentlyLost) { ctx.font = '24px sans-serif'; ctx.fillText('💀', canvas.width/2, canvas.height/2 - 35); ctx.fillStyle = '#ff0000'; ctx.font = 'bold 11px monospace'; ctx.fillText(signalEvent.title, canvas.width/2, canvas.height/2 - 10); ctx.fillStyle = '#ff4444'; ctx.font = '8px monospace'; ctx.fillText('PERMANENTLY LOST', canvas.width/2, canvas.height/2 + 5); // Time since loss if (signalEvent.timeSinceLoss) { const hours = Math.floor(signalEvent.timeSinceLoss / 3600000); const mins = Math.floor((signalEvent.timeSinceLoss % 3600000) / 60000); ctx.fillStyle = '#888'; // v7.80: WCAG AA contrast fix ctx.font = '7px monospace'; ctx.fillText(`Lost ${hours}h ${mins}m ago`, canvas.width/2, canvas.height/2 + 18); } // Red border ctx.strokeStyle = '#ff0000'; ctx.lineWidth = 3; ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4); // Remove click handler canvas.style.cursor = 'default'; canvas.onclick = null; return; } // v6.3.5: Handle refresh/reconnection events with special clean UI if (signalEvent.category === 'refresh' || signalEvent.isRefreshEvent) { // Animated reconnection spinner const spinAngle = (Date.now() / 500) % (Math.PI * 2); ctx.save(); ctx.translate(canvas.width/2, canvas.height/2 - 25); ctx.rotate(spinAngle); ctx.font = '20px sans-serif'; ctx.fillText(signalEvent.icon, 0, 7); ctx.restore(); ctx.fillStyle = signalEvent.color || '#00aaff'; ctx.font = 'bold 11px monospace'; ctx.fillText('RECONNECTING...', canvas.width/2, canvas.height/2 + 5); // Animated dots const dots = '.'.repeat(1 + Math.floor(Date.now() / 400) % 3); ctx.fillStyle = '#888'; ctx.font = '9px monospace'; ctx.fillText(signalEvent.message + dots, canvas.width/2, canvas.height/2 + 20); // Smooth progress bar for quick reconnection if (signalEvent.timeRemaining !== undefined) { const barWidth = 80; const barHeight = 4; const barX = (canvas.width - barWidth) / 2; const barY = canvas.height/2 + 32; const [minTime, maxTime] = signalEvent.recoveryTime || [3, 10]; const avgTime = (minTime + maxTime) / 2; const progress = Math.max(0, Math.min(1, 1 - (signalEvent.timeRemaining / avgTime))); // Background bar ctx.fillStyle = 'rgba(0, 170, 255, 0.2)'; ctx.fillRect(barX, barY, barWidth, barHeight); // Progress fill with gradient const gradient = ctx.createLinearGradient(barX, barY, barX + barWidth, barY); gradient.addColorStop(0, signalEvent.color || '#00aaff'); gradient.addColorStop(1, '#00ff88'); ctx.fillStyle = gradient; ctx.fillRect(barX, barY, barWidth * progress, barHeight); // ETA text const timeStr = SignalInterruptionSystem.formatTimeRemaining(signalEvent.timeRemaining); ctx.fillStyle = '#00aaff'; ctx.font = '8px monospace'; ctx.fillText(`ETA: ${timeStr}`, canvas.width/2, canvas.height/2 + 48); } // Pulsing cyan border const pulse = (Math.sin(Date.now() / 400) + 1) / 2; ctx.strokeStyle = `rgba(0, 170, 255, ${0.3 + pulse * 0.4})`; ctx.lineWidth = 2; ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4); canvas.style.cursor = 'default'; canvas.onclick = null; return; } // Event icon ctx.font = '20px sans-serif'; ctx.fillText(signalEvent.icon, canvas.width/2, canvas.height/2 - 35); // Event title ctx.fillStyle = signalEvent.color; ctx.font = 'bold 11px monospace'; ctx.fillText(signalEvent.title, canvas.width/2, canvas.height/2 - 12); // Event message ctx.fillStyle = '#888'; ctx.font = '9px monospace'; ctx.fillText(signalEvent.message, canvas.width/2, canvas.height/2 + 3); // v6.3.3: Show recovery countdown if recoverable if (signalEvent.recoverable && signalEvent.timeRemaining !== undefined) { const timeStr = SignalInterruptionSystem.formatTimeRemaining(signalEvent.timeRemaining); ctx.fillStyle = '#0af'; ctx.font = '8px monospace'; ctx.fillText(`Recovery ETA: ${timeStr}`, canvas.width/2, canvas.height/2 + 18); // Progress bar showing time remaining const barWidth = 60; const barHeight = 3; const barX = (canvas.width - barWidth) / 2; const barY = canvas.height/2 + 28; // Calculate actual progress based on recovery time const [minTime, maxTime] = signalEvent.recoveryTime || [30, 90]; const avgTime = (minTime + maxTime) / 2; const progress = Math.max(0, Math.min(1, 1 - (signalEvent.timeRemaining / avgTime))); ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(barX, barY, barWidth, barHeight); ctx.fillStyle = signalEvent.color; ctx.fillRect(barX, barY, barWidth * progress, barHeight); // Recovery chance indicator const chancePercent = Math.round((signalEvent.recoveryChance || 0.8) * 100); ctx.fillStyle = chancePercent >= 80 ? '#0f8' : (chancePercent >= 50 ? '#ff0' : '#f80'); ctx.font = '7px monospace'; ctx.fillText(`Success rate: ${chancePercent}%`, canvas.width/2, canvas.height/2 + 40); } else { // Non-recoverable or legacy display ctx.fillStyle = '#0af'; ctx.font = '8px monospace'; ctx.fillText(signalEvent.recovery, canvas.width/2, canvas.height/2 + 18); // Animated recovery bar (oscillating for unknown time) const barWidth = 60; const barHeight = 3; const barX = (canvas.width - barWidth) / 2; const barY = canvas.height/2 + 28; const progress = (Math.sin(Date.now() / 500) + 1) / 2; ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(barX, barY, barWidth, barHeight); ctx.fillStyle = signalEvent.color; ctx.fillRect(barX, barY, barWidth * progress, barHeight); } } else { // Draw simple status text ctx.fillStyle = '#0af'; ctx.font = 'bold 14px monospace'; ctx.textAlign = 'center'; ctx.fillText(message, canvas.width/2, canvas.height/2 - 10); } // v6.3.1: Show that agent IS working even without visual if (agent.status === 'working') { ctx.font = '11px monospace'; ctx.fillStyle = '#0f8'; ctx.fillText('AGENT ACTIVE', canvas.width/2, canvas.height/2 + (signalEvent ? 55 : 10)); // Show current action if (agent.statusMessage) { ctx.font = '9px monospace'; ctx.fillStyle = '#888'; const shortMsg = agent.statusMessage.substring(0, 30); ctx.fillText(shortMsg, canvas.width/2, canvas.height/2 + (signalEvent ? 62 : 25)); } } // Draw agent info if available (only if no signal event taking up space) if (!signalEvent && agent.taskState) { ctx.font = '10px monospace'; ctx.fillStyle = '#888'; const state = agent.taskState.state || 'initializing'; ctx.fillText(`State: ${state}`, canvas.width/2, canvas.height/2 + 40); } // Draw border with event-specific color ctx.strokeStyle = signalEvent ? signalEvent.color : '#0af'; ctx.lineWidth = 2; ctx.strokeRect(2, 2, canvas.width - 4, canvas.height - 4); } // ============================================ // v6.5.0: AGENT OBSERVER MODE (StarCraft-style) // Camera follows agent while they work autonomously // Press ESC or click "Return to Robot" to exit // ============================================ const agentObserverMode = { active: false, observedAgentId: null, savedCameraOffset: null, uiElement: null, beacon: null // v6.5.2: Glowing beacon pillar above observed agent }; // v6.5.0: Enter observer mode - camera follows agent, agent keeps working function focusOnAgent(agentId) { const agent = agentLookup.get(agentId); if (!agent) { addCopilotMessage(`Cannot locate agent - agent not found.`, 'ai'); return; } // v6.5.1: Close takeover mode if active - observer and takeover are mutually exclusive if (agentTakeoverState && agentTakeoverState.active) { closeAgentTakeover(); } // Try to create mesh if scene is ready and no mesh yet if (!agent.mesh && scene) { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`focusOnAgent: Creating mesh for ${agent.name}`); createAgentMesh(agent); } // Get agent position let agentPos; if (agent.mesh) { agentPos = agent.mesh.position; } else if (agent.position && (agent.position.x !== 0 || agent.position.z !== 0)) { agentPos = agent.position; } else { addCopilotMessage(`Cannot locate agent - no valid position. Try again after world loads.`, 'ai'); return; } // v6.5.0: ENTER OBSERVER MODE - don't teleport player! agentObserverMode.active = true; agentObserverMode.observedAgentId = agentId; // v6.5.2: Create glowing beacon pillar above agent so it's visible createObserverBeacon(agent); // Show observer UI showAgentObserverUI(agent); // Visual feedback spawnFloater(agentPos, `👁️ Observing ${agent.name}`, '#0af'); // Announce addCopilotMessage(`👁️ Now observing ${agent.typeConfig.icon} ${agent.name} - Press ESC or click button to return`, 'ai'); // Highlight on minimap highlightAgentOnMinimap(agent); } // v6.5.2: Create a tall glowing beacon above the observed agent function createObserverBeacon(agent) { // Remove existing beacon if any // v10.5: Proper beacon disposal (8-Agent Consensus Cycle 6) if (agentObserverMode.beacon && scene) { scene.remove(agentObserverMode.beacon); agentObserverMode.beacon.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material) child.material.dispose(); }); agentObserverMode.beacon = null; } if (!agent.mesh || !scene) return; const beaconGroup = new THREE.Group(); const color = agent.typeConfig.color || 0x00aaff; // Tall glowing pillar const pillarGeom = new THREE.CylinderGeometry(0.3, 0.8, 25, 8); const pillarMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.4 }); const pillar = new THREE.Mesh(pillarGeom, pillarMat); pillar.position.y = 12.5; beaconGroup.add(pillar); // Inner bright core const coreGeom = new THREE.CylinderGeometry(0.1, 0.3, 25, 6); const coreMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 }); const core = new THREE.Mesh(coreGeom, coreMat); core.position.y = 12.5; beaconGroup.add(core); // Glowing ring at base const ringGeom = new THREE.TorusGeometry(2, 0.3, 8, 16); const ringMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.6 }); const ring = new THREE.Mesh(ringGeom, ringMat); ring.rotation.x = Math.PI / 2; ring.position.y = 0.5; beaconGroup.add(ring); // Pulsing outer ring const outerRingGeom = new THREE.TorusGeometry(3.5, 0.15, 8, 24); const outerRingMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.3 }); const outerRing = new THREE.Mesh(outerRingGeom, outerRingMat); outerRing.rotation.x = Math.PI / 2; outerRing.position.y = 0.3; beaconGroup.add(outerRing); // Floating arrow pointing down const arrowGeom = new THREE.ConeGeometry(1.5, 3, 4); const arrowMat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 0.8 }); const arrow = new THREE.Mesh(arrowGeom, arrowMat); arrow.position.y = 8; arrow.rotation.z = Math.PI; // Point downward beaconGroup.add(arrow); // Store reference for animation beaconGroup.userData = { ring, outerRing, arrow, pillar }; scene.add(beaconGroup); agentObserverMode.beacon = beaconGroup; // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`Observer beacon created for ${agent.name}`); } // v6.5.2: Update beacon position to follow agent function updateObserverBeacon() { if (!agentObserverMode.active || !agentObserverMode.beacon) return; const agent = agentLookup.get(agentObserverMode.observedAgentId); if (!agent || !agent.mesh) return; // Move beacon to agent position agentObserverMode.beacon.position.copy(agent.mesh.position); // Animate beacon const time = Date.now() * 0.002; const userData = agentObserverMode.beacon.userData; if (userData.ring) { userData.ring.rotation.z = time; } if (userData.outerRing) { userData.outerRing.rotation.z = -time * 0.5; userData.outerRing.scale.setScalar(1 + Math.sin(time * 2) * 0.1); } if (userData.arrow) { userData.arrow.position.y = 8 + Math.sin(time * 3) * 1; } if (userData.pillar) { userData.pillar.material.opacity = 0.3 + Math.sin(time * 2) * 0.15; } } // v6.5.0: Exit observer mode - return camera to player function exitAgentObserverMode() { if (!agentObserverMode.active) return; const agent = agentLookup.get(agentObserverMode.observedAgentId); agentObserverMode.active = false; agentObserverMode.observedAgentId = null; // v6.5.2: Remove beacon // v10.5: Proper beacon disposal (8-Agent Consensus Cycle 6) if (agentObserverMode.beacon && scene) { scene.remove(agentObserverMode.beacon); agentObserverMode.beacon.traverse(child => { if (child.geometry) child.geometry.dispose(); if (child.material) child.material.dispose(); }); agentObserverMode.beacon = null; } // Hide observer UI hideAgentObserverUI(); // Announce if (agent) { addCopilotMessage(`Stopped observing ${agent.typeConfig.icon} ${agent.name} - Back to your robot`, 'ai'); } } // v6.5.0: Show observer mode UI overlay function showAgentObserverUI(agent) { hideAgentObserverUI(); // Remove any existing const ui = document.createElement('div'); ui.id = 'agent-observer-ui'; ui.style.cssText = ` position: fixed; top: 50%; left: 20px; transform: translateY(-50%); background: rgba(0, 20, 40, 0.95); border: 2px solid ${agent.typeConfig.color ? '#' + agent.typeConfig.color.toString(16).padStart(6, '0') : '#0af'}; border-radius: 12px; padding: 15px; z-index: 2000; color: #fff; font-family: 'Segoe UI', sans-serif; min-width: 200px; backdrop-filter: blur(10px); box-shadow: 0 0 30px rgba(0, 170, 255, 0.3); `; ui.innerHTML = `
${agent.typeConfig.icon}
OBSERVING
${agent.name}
${agent.statusMessage || 'Working...'}
Lv.${agent.agentLevel} | ${agent.taskState?.state || 'active'}
Press ESC to exit
`; document.body.appendChild(ui); agentObserverMode.uiElement = ui; // Update status periodically // v7.72: Use TimerRegistry for proper cleanup tracking const updateFn = () => { const statusEl = document.getElementById('observer-agent-status'); const statsEl = document.getElementById('observer-agent-stats'); if (statusEl && agent) { statusEl.textContent = agent.statusMessage || 'Working...'; } if (statsEl && agent) { const inv = agent.taskState?.inventory?.length || 0; const cap = agent.taskState?.carryingCapacity || 6; statsEl.textContent = `Lv.${agent.agentLevel} | ${agent.taskState?.state || 'active'} | 📦 ${inv}/${cap}`; } }; if (typeof TimerRegistry !== 'undefined') { TimerRegistry.setInterval('agent-observer-status', updateFn, 500); } else { agentObserverMode.statusInterval = setInterval(updateFn, 500); } } // v6.5.0: Hide observer mode UI // v7.72: Use TimerRegistry for proper cleanup function hideAgentObserverUI() { if (agentObserverMode.uiElement) { agentObserverMode.uiElement.remove(); agentObserverMode.uiElement = null; } if (typeof TimerRegistry !== 'undefined') { TimerRegistry.clear('agent-observer-status'); } else if (agentObserverMode.statusInterval) { clearInterval(agentObserverMode.statusInterval); agentObserverMode.statusInterval = null; } } // v6.5.0: Get the position the camera should follow (agent or player) function getObserverTargetPosition() { if (agentObserverMode.active) { const agent = agentLookup.get(agentObserverMode.observedAgentId); if (agent) { if (agent.mesh) return agent.mesh.position; if (agent.position) return agent.position; } // Agent lost, exit observer mode exitAgentObserverMode(); } return worldState.player ? worldState.player.position : null; } // v5.16.1: Highlight agent on minimap function highlightAgentOnMinimap(agent) { // The minimap is redrawn each frame, so we'll add a temporary highlight flag agent.minimapHighlight = true; setTimeout(() => { agent.minimapHighlight = false; }, 5000); // Highlight for 5 seconds } // v5.16.1: Create a beacon to guide player to agent // v6.3.1: Updated to work without mesh using agent.position function createAgentBeacon(agent) { if (!scene) return; // v6.3.1: Get position from mesh or agent.position const pos = agent.mesh ? agent.mesh.position : agent.position; if (!pos) return; // Create a vertical beam of light at agent's position const beaconGeom = new THREE.CylinderGeometry(0.1, 0.5, 15, 8, 1, true); const beaconMat = new THREE.MeshBasicMaterial({ color: agent.typeConfig.color, transparent: true, opacity: 0.3, side: THREE.DoubleSide }); const beacon = new THREE.Mesh(beaconGeom, beaconMat); beacon.position.copy(pos); beacon.position.y = 7.5; scene.add(beacon); // Animate and remove after 5 seconds let elapsed = 0; const animateBeacon = () => { elapsed += 16; if (elapsed > 5000) { scene.remove(beacon); return; } // Pulse effect const pulse = Math.sin(elapsed / 200) * 0.2 + 0.8; beacon.scale.set(pulse, 1, pulse); beaconMat.opacity = 0.3 * (1 - elapsed / 5000); // Follow agent if they move (prefer mesh position if available) const currentPos = agent.mesh ? agent.mesh.position : agent.position; if (currentPos) { beacon.position.x = currentPos.x; beacon.position.z = currentPos.z; } requestAnimationFrame(animateBeacon); }; animateBeacon(); } // v5.16.1: Update body cams for all expanded agent viewers // v8.20: Use for loop instead of forEach function updateAllAgentBodyCams() { const fleetLen = agentFleet.length; for (let i = 0; i < fleetLen; i++) { const agent = agentFleet[i]; const viewer = document.getElementById(`transcript-viewer-${agent.id}`); if (viewer && viewer.classList.contains('expanded')) { renderAgentBodyCam(agent); } } } // ============================================ // v5.16.2: AGENT TAKEOVER / REMOTE CONTROL SYSTEM // Allows full remote control of any agent in real-time // ============================================ // Takeover state let agentTakeoverState = { active: false, controlledAgentId: null, flyoutOpen: false, takeoverRenderer: null, takeoverCamera: null, takeoverKeys: { w: false, a: false, s: false, d: false }, lastRenderTime: 0 }; // Initialize takeover renderer (higher quality for flyout) function initTakeoverRenderer() { if (agentTakeoverState.takeoverRenderer) return; agentTakeoverState.takeoverCamera = new THREE.PerspectiveCamera(75, 400/300, 0.1, 200); agentTakeoverState.takeoverRenderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, powerPreference: 'high-performance' }); agentTakeoverState.takeoverRenderer.setSize(400, 300); agentTakeoverState.takeoverRenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); } // Open takeover flyout for an agent function openAgentTakeover(agentId) { const agent = agentLookup.get(agentId); if (!agent || !agent.mesh) { addCopilotMessage('Cannot takeover - agent not found or has no physical presence.', 'ai'); return; } // v6.5.1: Close observer mode if active - observer and takeover are mutually exclusive if (agentObserverMode && agentObserverMode.active) { exitAgentObserverMode(); } // Initialize renderer if needed initTakeoverRenderer(); // Set takeover state agentTakeoverState.active = true; agentTakeoverState.controlledAgentId = agentId; agentTakeoverState.flyoutOpen = true; // Pause agent's autonomous behavior if (agent.taskState) { agent.taskState.previousState = agent.taskState.state; agent.taskState.state = 'manual_control'; } // Create flyout UI createTakeoverFlyout(agent); // Notify addCopilotMessage(`🎮 TAKEOVER ACTIVE: Now controlling ${agent.typeConfig.icon} ${agent.name}. Use WASD to move.`, 'ai'); AudioSystem.play('powerup'); } // Create the takeover flyout UI function createTakeoverFlyout(agent) { // Remove any existing flyout const existing = document.getElementById('agent-takeover-flyout'); if (existing) existing.remove(); const taskState = agent.taskState || {}; const hp = taskState.hp || 50; const maxHp = taskState.maxHp || 50; const hpPercent = (hp / maxHp) * 100; const hpClass = hpPercent <= 25 ? 'critical' : (hpPercent <= 50 ? 'low' : ''); const flyout = document.createElement('div'); flyout.id = 'agent-takeover-flyout'; flyout.className = 'agent-takeover-flyout'; flyout.innerHTML = `
${agent.typeConfig.icon} REMOTE CONTROL: ${agent.name}
${Math.floor(hp)} / ${maxHp} HP
${agent.typeConfig.name}
${taskState.targetObject ? '🎯 ' + (taskState.targetObject.name || 'Target') : '🔍 No Target'}
${taskState.currentTask || 'MANUAL CONTROL'}
W
A
S
D
`; document.body.appendChild(flyout); // Add active indicator at top of screen const indicator = document.createElement('div'); indicator.id = 'takeover-active-indicator'; indicator.className = 'takeover-active-indicator'; indicator.innerHTML = ` 🎮 CONTROLLING: ${agent.typeConfig.icon} ${agent.name} [ESC to exit] `; document.body.appendChild(indicator); // Start rendering loop requestAnimationFrame(renderTakeoverView); // Add keyboard listener for takeover controls // v6.6: Remove existing listeners first to prevent memory leak (Agent 2 bug fix) window.removeEventListener('keydown', handleTakeoverKeyDown); window.removeEventListener('keyup', handleTakeoverKeyUp); window.addEventListener('keydown', handleTakeoverKeyDown); window.addEventListener('keyup', handleTakeoverKeyUp); } // Handle keyboard input for takeover mode function handleTakeoverKeyDown(e) { if (!agentTakeoverState.active) return; // v7.2: Skip if typing in input fields (chat, etc.) const activeElement = document.activeElement; if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable)) { return; } const key = e.key.toLowerCase(); // WASD movement if (key === 'w') { agentTakeoverState.takeoverKeys.w = true; e.preventDefault(); } if (key === 'a') { agentTakeoverState.takeoverKeys.a = true; e.preventDefault(); } if (key === 's') { agentTakeoverState.takeoverKeys.s = true; e.preventDefault(); } if (key === 'd') { agentTakeoverState.takeoverKeys.d = true; e.preventDefault(); } // Update WASD indicator UI updateTakeoverWASDUI(); // Action key if (key === 'e') { takeoverAgentAction(); e.preventDefault(); } // Locate key if (key === 'l') { focusOnControlledAgent(); e.preventDefault(); } // Auto mode toggle if (key === 'm') { toggleTakeoverAutoMode(); e.preventDefault(); } // Escape to exit if (key === 'escape') { closeAgentTakeover(); e.preventDefault(); } } // Handle keyboard release for takeover mode function handleTakeoverKeyUp(e) { if (!agentTakeoverState.active) return; const key = e.key.toLowerCase(); if (key === 'w') agentTakeoverState.takeoverKeys.w = false; if (key === 'a') agentTakeoverState.takeoverKeys.a = false; if (key === 's') agentTakeoverState.takeoverKeys.s = false; if (key === 'd') agentTakeoverState.takeoverKeys.d = false; updateTakeoverWASDUI(); } // Update WASD visual indicator function updateTakeoverWASDUI() { const keys = agentTakeoverState.takeoverKeys; const wKey = document.getElementById('takeover-key-w'); const aKey = document.getElementById('takeover-key-a'); const sKey = document.getElementById('takeover-key-s'); const dKey = document.getElementById('takeover-key-d'); if (wKey) wKey.classList.toggle('active', keys.w); if (aKey) aKey.classList.toggle('active', keys.a); if (sKey) sKey.classList.toggle('active', keys.s); if (dKey) dKey.classList.toggle('active', keys.d); } // Render the takeover POV view function renderTakeoverView() { if (!agentTakeoverState.active || !agentTakeoverState.flyoutOpen) return; const agent = agentLookup.get(agentTakeoverState.controlledAgentId); if (!agent || !agent.mesh || !scene) { closeAgentTakeover(); return; } const now = performance.now(); const deltaTime = (now - agentTakeoverState.lastRenderTime) / 1000; agentTakeoverState.lastRenderTime = now; // Process movement input processAgentTakeoverMovement(agent, deltaTime); // Update HUD updateTakeoverHUD(agent); // Position camera at agent's POV const agentPos = agent.mesh.position; const agentRotation = agent.mesh.rotation.y; // v7.37: Use pre-allocated vectors for camera positioning (Cycle 16 Performance) _agentCamOffset.set( Math.sin(agentRotation) * 0.3, 1.5, // Eye level Math.cos(agentRotation) * 0.3 ); agentTakeoverState.takeoverCamera.position.copy(agentPos).add(_agentCamOffset); // Look in the direction the agent is facing _agentCamLookTarget.set( agentPos.x + Math.sin(agentRotation) * 10, agentPos.y + 1.2, agentPos.z + Math.cos(agentRotation) * 10 ); agentTakeoverState.takeoverCamera.lookAt(_agentCamLookTarget); // Render to canvas const canvas = document.getElementById('takeover-canvas'); if (canvas && agentTakeoverState.takeoverRenderer) { // Update renderer size to match canvas const rect = canvas.parentElement.getBoundingClientRect(); const width = Math.floor(rect.width); const height = Math.floor(rect.height); if (width > 0 && height > 0) { agentTakeoverState.takeoverCamera.aspect = width / height; agentTakeoverState.takeoverCamera.updateProjectionMatrix(); agentTakeoverState.takeoverRenderer.setSize(width, height); } agentTakeoverState.takeoverRenderer.render(scene, agentTakeoverState.takeoverCamera); // Copy to visible canvas const ctx = canvas.getContext('2d'); if (ctx) { canvas.width = width; canvas.height = height; ctx.drawImage(agentTakeoverState.takeoverRenderer.domElement, 0, 0); // Add scan line overlay for visual effect ctx.strokeStyle = 'rgba(255, 136, 0, 0.02)'; for (let y = 0; y < height; y += 4) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } } } // Continue render loop requestAnimationFrame(renderTakeoverView); } // Process movement input for controlled agent function processAgentTakeoverMovement(agent, deltaTime) { const keys = agentTakeoverState.takeoverKeys; const hasInput = keys.w || keys.a || keys.s || keys.d; if (!hasInput) return; const speed = 8 * deltaTime; // Agent movement speed const moveDir = new THREE.Vector3(0, 0, 0); // Calculate movement relative to agent's facing direction const agentRotation = agent.mesh.rotation.y; if (keys.w) { moveDir.x += Math.sin(agentRotation) * speed; moveDir.z += Math.cos(agentRotation) * speed; } if (keys.s) { moveDir.x -= Math.sin(agentRotation) * speed; moveDir.z -= Math.cos(agentRotation) * speed; } if (keys.a) { moveDir.x += Math.cos(agentRotation) * speed; moveDir.z -= Math.sin(agentRotation) * speed; } if (keys.d) { moveDir.x -= Math.cos(agentRotation) * speed; moveDir.z += Math.sin(agentRotation) * speed; } // Apply movement agent.mesh.position.add(moveDir); // Update facing direction based on movement if (moveDir.length() > 0.01) { const newRotation = Math.atan2(moveDir.x, moveDir.z); agent.mesh.rotation.y = newRotation; } // Snap to ground if (typeof snapToGround === 'function') { snapToGround(agent.mesh); } // Update task state position tracking // v8.23: Use copy() instead of clone() to avoid allocation if (agent.taskState) { if (!agent.taskState.lastPosition) { agent.taskState.lastPosition = new THREE.Vector3(); } agent.taskState.lastPosition.copy(agent.mesh.position); } } // Update takeover HUD elements function updateTakeoverHUD(agent) { const taskState = agent.taskState || {}; const hp = taskState.hp || 50; const maxHp = taskState.maxHp || 50; const hpPercent = (hp / maxHp) * 100; // Update HP bar const hpFill = document.getElementById('takeover-hp-fill'); const hpText = document.getElementById('takeover-hp-text'); if (hpFill) { hpFill.style.width = hpPercent + '%'; hpFill.className = 'takeover-hp-fill' + (hpPercent <= 25 ? ' critical' : (hpPercent <= 50 ? ' low' : '')); } if (hpText) { hpText.textContent = `${Math.floor(hp)} / ${maxHp} HP`; } // Update target info const targetInfo = document.getElementById('takeover-target-info'); if (targetInfo) { if (taskState.targetObject) { targetInfo.textContent = '🎯 ' + (taskState.targetObject.name || taskState.targetObject.type || 'Target'); targetInfo.className = 'takeover-target-info'; } else { targetInfo.textContent = '🔍 No Target'; targetInfo.className = 'takeover-target-info resource'; } } // Update status badge const statusBadge = document.getElementById('takeover-status'); if (statusBadge) { statusBadge.textContent = taskState.state === 'manual_control' ? 'MANUAL CONTROL' : (taskState.currentTask || taskState.state || 'ACTIVE').toUpperCase(); } } // Perform agent action (interact with nearby objects) function takeoverAgentAction() { const agent = agentLookup.get(agentTakeoverState.controlledAgentId); if (!agent || !agent.mesh) return; const agentPos = agent.mesh.position; // Find nearest interactable object let nearestDist = Infinity; let nearestObject = null; // Check resources - v7.78: distanceToSquared optimization // v8.16: forEach-to-for optimization if (worldState.resources) { const resources = worldState.resources; for (let ri = 0, rlen = resources.length; ri < rlen; ri++) { const resource = resources[ri]; if (!resource.position) continue; const distSq = agentPos.distanceToSquared(resource.position); if (distSq < 9 && distSq < nearestDist) { // 3*3=9 nearestDist = distSq; nearestObject = resource; } } } // Check mobs - v7.78: distanceToSquared optimization // v8.04: forEach to for loop conversion (agent combat) if (worldState.mobs) { const agentMobs = worldState.mobs; for (let ami = 0, amlen = agentMobs.length; ami < amlen; ami++) { const mob = agentMobs[ami]; if (!mob.mesh || mob.isDead) continue; const distSq = agentPos.distanceToSquared(mob.mesh.position); if (distSq < 9 && distSq < nearestDist) { // 3*3=9 nearestDist = distSq; nearestObject = { type: 'mob', mob: mob }; } } } if (nearestObject) { if (nearestObject.type === 'mob') { // Attack mob const mob = nearestObject.mob; const damage = 15 + Math.random() * 10; mob.hp -= damage; spawnFloater(mob.mesh.position, `-${Math.floor(damage)}`, '#f44'); AudioSystem.play('hit'); if (mob.hp <= 0) { mob.isDead = true; addCopilotMessage(`${agent.typeConfig.icon} ${agent.name} defeated ${mob.type}!`, 'ai'); } } else { // Harvest resource if (typeof performAgentAction === 'function') { performAgentAction(agent, nearestObject); } else { spawnFloater(agentPos, '+1 Resource', '#0f0'); } AudioSystem.play('collect'); } } else { spawnFloater(agentPos, 'Nothing nearby', '#888'); } } // Focus main camera on the controlled agent function focusOnControlledAgent() { if (!agentTakeoverState.controlledAgentId) return; focusOnAgent(agentTakeoverState.controlledAgentId); } // Toggle auto mode (return agent to autonomous behavior) function toggleTakeoverAutoMode() { const agent = agentLookup.get(agentTakeoverState.controlledAgentId); if (!agent) return; if (agent.taskState && agent.taskState.state === 'manual_control') { // Switch to auto mode agent.taskState.state = agent.taskState.previousState || 'idle'; const statusBadge = document.getElementById('takeover-status'); if (statusBadge) statusBadge.textContent = 'AUTO MODE'; addCopilotMessage(`${agent.typeConfig.icon} ${agent.name} switched to AUTO MODE - watching only`, 'ai'); } else if (agent.taskState) { // Switch back to manual agent.taskState.previousState = agent.taskState.state; agent.taskState.state = 'manual_control'; const statusBadge = document.getElementById('takeover-status'); if (statusBadge) statusBadge.textContent = 'MANUAL CONTROL'; addCopilotMessage(`${agent.typeConfig.icon} ${agent.name} switched to MANUAL CONTROL`, 'ai'); } } // Close takeover and return to robot function closeAgentTakeover() { const agent = agentLookup.get(agentTakeoverState.controlledAgentId); // Restore agent's autonomous state if (agent && agent.taskState) { if (agent.taskState.state === 'manual_control') { agent.taskState.state = agent.taskState.previousState || 'idle'; } } // Reset takeover state agentTakeoverState.active = false; agentTakeoverState.controlledAgentId = null; agentTakeoverState.flyoutOpen = false; agentTakeoverState.takeoverKeys = { w: false, a: false, s: false, d: false }; // Remove UI elements const flyout = document.getElementById('agent-takeover-flyout'); if (flyout) flyout.remove(); const indicator = document.getElementById('takeover-active-indicator'); if (indicator) indicator.remove(); // Remove keyboard listeners window.removeEventListener('keydown', handleTakeoverKeyDown); window.removeEventListener('keyup', handleTakeoverKeyUp); // Notify if (agent) { addCopilotMessage(`🔙 Returned control to main robot. ${agent.typeConfig.icon} ${agent.name} resuming autonomous operations.`, 'ai'); } AudioSystem.play('ui'); } // Check if any agent is being controlled (for game loop) function isAgentTakeoverActive() { return agentTakeoverState.active && agentTakeoverState.controlledAgentId !== null; } // v5.17.1: Pop-out Agent Control Window System // Allows controlling agents in separate windows without interrupting main game const agentPopOutWindows = new Map(); // agentId -> window reference function popOutAgentWindow(agentId) { const agent = agentLookup.get(agentId); if (!agent || !agent.mesh) { addCopilotMessage('Cannot open pop-out - agent not found or has no physical presence.', 'ai'); return; } // Check if window already exists if (agentPopOutWindows.has(agentId)) { const existingWindow = agentPopOutWindows.get(agentId); if (existingWindow && !existingWindow.closed) { existingWindow.focus(); return; } } // Create the pop-out window const windowWidth = 500; const windowHeight = 450; const left = window.screenX + 50 + (agentPopOutWindows.size * 30); const top = window.screenY + 50 + (agentPopOutWindows.size * 30); const popOutWindow = window.open('', `agent_${agentId}`, `width=${windowWidth},height=${windowHeight},left=${left},top=${top},resizable=yes` ); if (!popOutWindow) { addCopilotMessage('Pop-up blocked! Please allow pop-ups for this site.', 'ai'); return; } agentPopOutWindows.set(agentId, popOutWindow); // Get the HTML components from generateAgentPopOutHTML const htmlParts = generateAgentPopOutHTML(agent); // v6.5.2: Build DOM directly to avoid document.write parsing issues // Wait for about:blank to be ready setTimeout(() => { if (!popOutWindow || popOutWindow.closed) return; const doc = popOutWindow.document; // Set title doc.title = htmlParts.title; // Add meta tags const meta1 = doc.createElement('meta'); meta1.charset = 'UTF-8'; doc.head.appendChild(meta1); const meta2 = doc.createElement('meta'); meta2.name = 'viewport'; meta2.content = 'width=device-width, initial-scale=1.0'; doc.head.appendChild(meta2); // Add styles const style = doc.createElement('style'); style.textContent = htmlParts.css; doc.head.appendChild(style); // Add body content doc.body.innerHTML = htmlParts.body; // Set up communication bridge popOutWindow.agentId = agentId; popOutWindow.parentGame = window; // Inject script const scriptEl = doc.createElement('script'); scriptEl.textContent = htmlParts.js; doc.body.appendChild(scriptEl); }, 50); // v7.79: Handle window close - migrated to TimerRegistry const closedCheckTimer = 'agent-popout-check-' + agentId; TimerRegistry.setInterval(closedCheckTimer, () => { if (popOutWindow.closed) { TimerRegistry.clear(closedCheckTimer); agentPopOutWindows.delete(agentId); } }, 1000); addCopilotMessage(`🪟 ${agent.typeConfig.icon} ${agent.name} control window opened! Main game continues independently.`, 'ai'); AudioSystem.play('ui'); } function generateAgentPopOutHTML(agent) { const colorHex = '#' + agent.typeConfig.color.toString(16).padStart(6, '0'); const taskState = agent.taskState || {}; const hp = taskState.hp || 50; const maxHp = taskState.maxHp || 50; const hpPercent = (hp / maxHp) * 100; // Build CSS const css = ` * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Consolas', 'Monaco', monospace; background: #0a0a0f; color: #fff; overflow: hidden; user-select: none; } .header { background: linear-gradient(135deg, #1a1a2e 0%, #0d0d15 100%); border-bottom: 1px solid ${colorHex}44; padding: 8px 12px; display: flex; justify-content: space-between; align-items: center; } .agent-title { display: flex; align-items: center; gap: 8px; } .agent-icon { font-size: 24px; } .agent-name { font-size: 14px; font-weight: bold; color: ${colorHex}; } .agent-type { font-size: 11px; color: #aaa; } .status-badge { background: ${colorHex}33; border: 1px solid ${colorHex}; color: ${colorHex}; padding: 3px 8px; border-radius: 4px; font-size: 10px; text-transform: uppercase; } .viewport { position: relative; width: 100%; height: 280px; background: #000; border-bottom: 1px solid #333; } #agent-canvas { width: 100%; height: 100%; display: block; } .hud-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; } .hud-top { position: absolute; top: 8px; left: 8px; right: 8px; display: flex; justify-content: space-between; } .hp-container { background: rgba(0,0,0,0.7); padding: 6px 10px; border-radius: 4px; border: 1px solid #333; } .hp-bar { width: 120px; height: 8px; background: #222; border-radius: 4px; overflow: hidden; margin-bottom: 4px; } .hp-fill { height: 100%; background: linear-gradient(90deg, #f44 0%, #ff8800 50%, #4f4 100%); transition: width 0.3s; } .hp-fill.critical { background: #f44; animation: critical-pulse 0.5s infinite; } @keyframes critical-pulse { 0%,100%{opacity:1} 50%{opacity:0.5} } .hp-text { font-size: 10px; color: #aaa; } .stats-container { background: rgba(0,0,0,0.7); padding: 6px 10px; border-radius: 4px; border: 1px solid #333; text-align: right; } .stat-row { font-size: 10px; color: #aaa; margin: 2px 0; } .stat-value { color: ${colorHex}; font-weight: bold; } .stat-combo { color: #ff8800; } .hud-bottom { position: absolute; bottom: 8px; left: 8px; right: 8px; display: flex; justify-content: space-between; align-items: flex-end; } .coords { font-size: 10px; color: #0ff; font-family: monospace; } .action-text { font-size: 11px; color: #fff; } .controls { background: linear-gradient(180deg, #0d0d15 0%, #1a1a2e 100%); padding: 12px; display: flex; flex-direction: column; gap: 10px; } .control-row { display: flex; justify-content: center; gap: 8px; } .wasd-container { display: grid; grid-template-columns: repeat(3, 40px); grid-template-rows: repeat(2, 40px); gap: 4px; } /* v7.81: #444 to #666 for WCAG visibility */ .wasd-key { width: 40px; height: 40px; background: #222; border: 1px solid #666; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; color: #aaa; transition: all 0.1s; } .wasd-key.active { background: ${colorHex}44; border-color: ${colorHex}; color: ${colorHex}; box-shadow: 0 0 10px ${colorHex}66; } .action-btn { flex: 1; padding: 12px; background: #222; border: 1px solid #666; border-radius: 6px; color: #fff; font-size: 12px; font-weight: bold; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; gap: 6px; } .action-btn:hover { background: #333; border-color: #999; } .action-btn:active { transform: scale(0.97); } .action-btn.primary { border-color: ${colorHex}; color: ${colorHex}; } .action-btn.primary:hover { background: ${colorHex}22; } .keybind { font-size: 9px; color: #999; margin-left: 4px; } .mode-toggle { display: flex; align-items: center; gap: 8px; font-size: 11px; color: #aaa; } .mode-switch { width: 40px; height: 20px; background: #333; border-radius: 10px; position: relative; cursor: pointer; } .mode-switch::after { content: ''; position: absolute; width: 16px; height: 16px; background: #666; border-radius: 50%; top: 2px; left: 2px; transition: all 0.2s; } .mode-switch.auto::after { left: 22px; background: ${colorHex}; } .mode-switch.auto { background: ${colorHex}44; } `; // Build HTML body const body = `
${agent.typeConfig.icon}
${agent.name}
${agent.typeConfig.name}
AUTONOMOUS
${Math.floor(hp)} / ${maxHp} HP
Level: ${agent.agentLevel}
Combo: ${agent.combo}x
Efficiency: ${Math.round(agent.efficiency * 100)}%
X: 0 Z: 0
${agent.statusMessage || 'Working...'}
W
A
S
D
Manual
Auto
`; // Build JS - using array join to avoid script tag issues const js = [ 'const agentId = "' + agent.id + '";', 'let keys = { w: false, a: false, s: false, d: false };', 'let isAutoMode = true;', 'let canvas, ctx;', 'let updateInterval;', '', 'window.onload = function() {', ' canvas = document.getElementById("agent-canvas");', ' ctx = canvas.getContext("2d");', ' resizeCanvas();', ' window.onresize = resizeCanvas;', ' updateInterval = setInterval(updateView, 50);', ' document.addEventListener("keydown", handleKeyDown);', ' document.addEventListener("keyup", handleKeyUp);', ' document.getElementById("action-btn").onclick = performAction;', ' document.getElementById("locate-btn").onclick = locateAgent;', ' document.getElementById("mode-switch").onclick = toggleMode;', '};', '', 'function resizeCanvas() {', ' const viewport = canvas.parentElement;', ' canvas.width = viewport.clientWidth;', ' canvas.height = viewport.clientHeight;', '}', '', 'function updateView() {', ' if (!window.opener || window.opener.closed) {', ' clearInterval(updateInterval);', ' document.body.innerHTML = "
Main game window closed
";', ' return;', ' }', ' try {', ' const parent = window.opener;', ' const agent = parent.agentLookup && parent.agentLookup.get(agentId);', // v8.18: O(1) lookup ' if (!agent) {', ' ctx.fillStyle = "#111";', ' ctx.fillRect(0, 0, canvas.width, canvas.height);', ' ctx.fillStyle = "#f44";', ' ctx.font = "14px monospace";', ' ctx.fillText("Agent recalled", canvas.width/2 - 50, canvas.height/2);', ' return;', ' }', ' if (parent.renderPopOutAgentView) {', ' parent.renderPopOutAgentView(agentId, canvas);', ' }', ' updateHUD(agent);', ' if (!isAutoMode && (keys.w || keys.a || keys.s || keys.d)) {', ' moveAgent(agent);', ' }', ' } catch (e) { console.error("Update error:", e); }', '}', '', 'function updateHUD(agent) {', ' var taskState = agent.taskState || {};', ' var hp = taskState.hp || 50;', ' var maxHp = taskState.maxHp || 50;', ' var hpPercent = (hp / maxHp) * 100;', ' document.getElementById("hp-fill").style.width = hpPercent + "%";', ' document.getElementById("hp-fill").className = "hp-fill" + (hpPercent <= 25 ? " critical" : "");', ' document.getElementById("hp-text").textContent = Math.floor(hp) + " / " + maxHp + " HP";', ' document.getElementById("stat-level").textContent = agent.agentLevel;', ' document.getElementById("stat-combo").textContent = agent.combo + "x";', ' document.getElementById("stat-eff").textContent = Math.round(agent.efficiency * 100) + "%";', ' if (agent.mesh) {', ' var pos = agent.mesh.position;', ' document.getElementById("coords").textContent = "X: " + Math.floor(pos.x) + " Z: " + Math.floor(pos.z);', ' }', ' document.getElementById("action-text").textContent = agent.statusMessage || "Working...";', ' document.getElementById("status-badge").textContent = isAutoMode ? "AUTONOMOUS" : "MANUAL CONTROL";', '}', '', 'function moveAgent(agent) {', ' if (!agent.mesh || !window.opener) return;', ' var speed = 0.15;', ' var rotation = agent.mesh.rotation.y;', ' var dx = 0, dz = 0;', ' if (keys.w) { dx += Math.sin(rotation); dz += Math.cos(rotation); }', ' if (keys.s) { dx -= Math.sin(rotation); dz -= Math.cos(rotation); }', ' if (keys.a) { dx += Math.cos(rotation); dz -= Math.sin(rotation); }', ' if (keys.d) { dx -= Math.cos(rotation); dz += Math.sin(rotation); }', ' if (dx !== 0 || dz !== 0) {', ' var len = Math.sqrt(dx*dx + dz*dz);', ' dx = (dx / len) * speed;', ' dz = (dz / len) * speed;', ' agent.mesh.position.x += dx;', ' agent.mesh.position.z += dz;', ' agent.position.copy(agent.mesh.position);', ' agent.mesh.rotation.y = Math.atan2(dx, dz);', ' }', '}', '', 'function handleKeyDown(e) {', ' var key = e.key.toLowerCase();', ' if (["w","a","s","d"].indexOf(key) >= 0) {', ' keys[key] = true;', ' document.getElementById("key-" + key).classList.add("active");', ' if (isAutoMode) {', ' isAutoMode = false;', ' setAgentManualMode(true);', ' document.getElementById("mode-switch").classList.remove("auto");', ' }', ' e.preventDefault();', ' } else if (key === "e") { performAction(); }', ' else if (key === "l") { locateAgent(); }', ' else if (key === "m") { toggleMode(); }', '}', '', 'function handleKeyUp(e) {', ' var key = e.key.toLowerCase();', ' if (["w","a","s","d"].indexOf(key) >= 0) {', ' keys[key] = false;', ' document.getElementById("key-" + key).classList.remove("active");', ' }', '}', '', 'function toggleMode() {', ' isAutoMode = !isAutoMode;', ' document.getElementById("mode-switch").classList.toggle("auto", isAutoMode);', ' setAgentManualMode(!isAutoMode);', '}', '', 'function setAgentManualMode(isManual) {', ' try {', ' var parent = window.opener;', ' var agent = parent.agentLookup && parent.agentLookup.get(agentId);', // v8.18: O(1) lookup ' if (agent && agent.taskState) {', ' if (isManual) {', ' agent.taskState.previousState = agent.taskState.state;', ' agent.taskState.state = "manual_control";', ' } else {', ' agent.taskState.state = agent.taskState.previousState || "idle";', ' }', ' }', ' } catch (e) {}', '}', '', 'function performAction() {', ' try {', ' var parent = window.opener;', ' if (parent.popOutAgentAction) { parent.popOutAgentAction(agentId); }', ' } catch (e) {}', '}', '', 'function locateAgent() {', ' try {', ' var parent = window.opener;', ' if (parent.focusOnAgent) { parent.focusOnAgent(agentId); }', ' } catch (e) {}', '}', '', 'window.onbeforeunload = function() {', ' clearInterval(updateInterval);', ' setAgentManualMode(false);', '};' ].join('\n'); return { css, body, js, title: agent.typeConfig.icon + ' ' + agent.name + ' - Agent Control' }; } // Render function called by pop-out windows to get agent view function renderPopOutAgentView(agentId, targetCanvas) { const agent = agentLookup.get(agentId); if (!agent || !agent.mesh || !scene) return; const ctx = targetCanvas.getContext('2d'); // Use the existing body cam renderer if (!bodyCamInitialized) initAgentBodyCamSystem(); if (!agentBodyCamRenderer || !agentBodyCamCamera) { ctx.fillStyle = '#111'; ctx.fillRect(0, 0, targetCanvas.width, targetCanvas.height); return; } try { // Position camera at agent's POV const agentPos = agent.mesh.position; const agentRotation = agent.mesh.rotation.y || 0; agentBodyCamCamera.position.set( agentPos.x + Math.sin(agentRotation) * 0.3, agentPos.y + 1.5, agentPos.z + Math.cos(agentRotation) * 0.3 ); const lookTarget = new THREE.Vector3( agentPos.x + Math.sin(agentRotation) * 10, agentPos.y + 1.0, agentPos.z + Math.cos(agentRotation) * 10 ); agentBodyCamCamera.lookAt(lookTarget); // Adjust renderer for pop-out size const origWidth = agentBodyCamRenderer.domElement.width; const origHeight = agentBodyCamRenderer.domElement.height; agentBodyCamRenderer.setSize(targetCanvas.width, targetCanvas.height); agentBodyCamRenderer.render(scene, agentBodyCamCamera); // Copy to target canvas ctx.drawImage(agentBodyCamRenderer.domElement, 0, 0); // Reset renderer size agentBodyCamRenderer.setSize(origWidth, origHeight); } catch (e) { ctx.fillStyle = '#111'; ctx.fillRect(0, 0, targetCanvas.width, targetCanvas.height); } } // Action function called by pop-out windows function popOutAgentAction(agentId) { const agent = agentLookup.get(agentId); if (!agent || !agent.mesh) return; const agentPos = agent.mesh.position; let nearestDist = Infinity; let nearestObject = null; // Check resources - v7.78: distanceToSquared optimization // v8.09: forEach to for loop + InteractableSpatialGrid for O(1) lookup if (worldState.interactables) { const nearbyRes = InteractableSpatialGrid.getNearby(agentPos.x, agentPos.z, 1); for (let ri = 0, rlen = nearbyRes.length; ri < rlen; ri++) { const obj = nearbyRes[ri]; if (!obj.parent) continue; const distSq = agentPos.distanceToSquared(obj.position); if (distSq < 9 && distSq < nearestDist) { // 3*3=9 nearestDist = distSq; nearestObject = { type: 'resource', obj: obj }; } } } // Check mobs - v7.78: distanceToSquared optimization // v8.04: forEach to for loop conversion (agent action combat) if (worldState.mobs) { const actionMobs = worldState.mobs; for (let aci = 0, aclen = actionMobs.length; aci < aclen; aci++) { const mob = actionMobs[aci]; if (!mob.mesh || mob.isDead) continue; const distSq = agentPos.distanceToSquared(mob.mesh.position); if (distSq < 9 && distSq < nearestDist) { // 3*3=9 nearestDist = distSq; nearestObject = { type: 'mob', mob: mob }; } } } if (nearestObject) { if (nearestObject.type === 'mob') { const damage = 15 + agent.agentLevel * 2; nearestObject.mob.hp -= damage; spawnFloater(nearestObject.mob.mesh.position, '-' + Math.floor(damage), '#f44'); trackAgentAction(agent, true, 8); if (nearestObject.mob.hp <= 0) { nearestObject.mob.isDead = true; } } else { performAgentAction(agent, nearestObject.obj); } AudioSystem.play('hit'); } } // v5.16: Update all agent meshes and run autonomous task behaviors // v5.16.3: Fixed - create mesh if agent was spawned before scene was ready // v8.06: Converted forEach to for loop (hot path optimization) function updateAgentFleetMeshes(deltaTime) { const now = performance.now(); for (let i = 0, len = agentFleet.length; i < len; i++) { const agent = agentFleet[i]; // v5.16.3: If agent has no mesh but scene is now ready, create it if (!agent.mesh && scene) { createAgentMesh(agent); if (agent.mesh) { logAgentTask(agent, 'Spawned into world'); agent.statusMessage = 'Ready for duty!'; updateAgentCardUI(agent); } } if (!agent.mesh) continue; if (!agent.taskState) { // Initialize task state if missing (for older agents) // v7.86: Added _targetVec for clone() reduction // v8.21: Added _lastPosVec for position tracking (avoids clone() per agent per frame) agent.taskState = { currentTask: null, targetObject: null, targetPosition: null, state: 'idle', stuckCounter: 0, lastPosition: null, lastTaskTime: 0, alert: null, actionCooldown: 0, hp: 50, maxHp: 50, taskLog: [], _targetVec: new THREE.Vector3(), // v7.86: Pre-allocated for setAgentTarget _lastPosVec: new THREE.Vector3() // v8.21: Pre-allocated for stuck detection }; } const task = agent.taskState; const time = now / 1000; // Visual animations updateAgentVisuals(agent, deltaTime, time); // Run autonomous task behavior (every 100ms for performance) if (now - task.lastTaskTime > 100) { task.lastTaskTime = now; runAgentAutonomousTask(agent, deltaTime); } // v5.17: Agent health regeneration updateAgentHealthRegen(agent); // Check if stuck - v7.78: distanceToSquared optimization // v8.21: Use pre-allocated _lastPosVec with copy() instead of clone() per agent per frame if (!task._lastPosVec) task._lastPosVec = new THREE.Vector3(); if (task.lastPosition) { const movedSq = agent.mesh.position.distanceToSquared(task._lastPosVec); // v6.4.0: Also check stuck when returning to ship if (movedSq < 0.0001 && (task.state === 'moving' || task.state === 'returning')) { // 0.01*0.01=0.0001 task.stuckCounter++; if (task.stuckCounter > 50) { logAgentTask(agent, 'Got stuck, picking new target'); task.targetObject = null; task.targetPosition = null; task.state = 'idle'; task.stuckCounter = 0; } } else { task.stuckCounter = 0; } } task._lastPosVec.copy(agent.mesh.position); task.lastPosition = true; // v8.21: Flag that we have a valid lastPosition } } // v7.90: Pre-allocated temp objects for agent visual updates (hot path) const _agentVisualsTemp = { _dir: null, _color: null, init() { if (!this._dir) this._dir = new THREE.Vector3(); if (!this._color) this._color = new THREE.Color(); } }; // v5.16: Update agent visual animations function updateAgentVisuals(agent, deltaTime, time) { // Idle bobbing (subtle) const bobOffset = Math.sin(time * 2 + agent.id.charCodeAt(0)) * 0.05; agent.mesh.position.y = bobOffset; // Glow pulse if (agent.glow) { agent.glow.material.opacity = 0.08 + Math.sin(time * 3) * 0.04; } // Visor blink - v12.26: Check emissive support if (agent.visor && agent.visor.material?.emissiveIntensity !== undefined && Math.random() < 0.002) { agent.visor.material.emissiveIntensity = 0.2; setTimeout(() => { if (agent.visor?.material?.emissiveIntensity !== undefined) agent.visor.material.emissiveIntensity = 0.8; }, 100); } // v7.33: State-based visor color for instant task feedback (8-Strategy Cycle 12 - Robot Mode) // Makes Robot Command Mode engaging to watch - instant visual state recognition // v12.26: Check emissive support to prevent MeshBasicMaterial warnings if (agent.visor && agent.taskState && agent.visor.material?.emissive) { const stateColors = { idle: agent.typeConfig?.color || 0x00ff00, // Original type color moving: 0x00ffff, // Cyan - in transit working: 0x00ff00, // Green - productive combat: 0xff0000, // Red - danger returning: 0xffd700, // Gold - hauling back depositing: 0x88ff88, // Bright green - delivering alert: 0xff8800, // Orange - needs attention stuck: 0x888888, // Gray - problem manual_control: 0xff00ff // Magenta - player controlled }; const targetColor = stateColors[agent.taskState.state] || (agent.typeConfig?.color || 0x00ff00); // v7.90: Use pre-allocated Color object instead of new THREE.Color() per agent per frame _agentVisualsTemp.init(); _agentVisualsTemp._color.setHex(targetColor); // Smooth color transition agent.visor.material.emissive.lerp(_agentVisualsTemp._color, deltaTime * 5); // Pulse intensity for combat/alert states if (agent.taskState.state === 'combat') { agent.visor.material.emissiveIntensity = 1.2 + Math.sin(time * 10) * 0.4; } else if (agent.taskState.state === 'alert') { agent.visor.material.emissiveIntensity = 0.6 + Math.sin(time * 6) * 0.4; } else if (agent.visor.material.emissiveIntensity !== 0.8) { // Lerp back to normal agent.visor.material.emissiveIntensity = THREE.MathUtils.lerp( agent.visor.material.emissiveIntensity, 0.8, deltaTime * 3 ); } } // Alert indicator pulsing if (agent.alertIndicator && agent.taskState.alert) { agent.alertIndicator.material.opacity = 0.5 + Math.sin(time * 6) * 0.5; agent.alertIndicator.scale.setScalar(1 + Math.sin(time * 6) * 0.3); } else if (agent.alertIndicator) { agent.alertIndicator.material.opacity = 0; } // Tool animation when working if (agent.tool && agent.taskState.state === 'working') { agent.tool.rotation.x = Math.sin(time * 8) * 0.3; } // Face movement direction if (agent.taskState.targetPosition) { // v7.90: Use pre-allocated Vector3 instead of new THREE.Vector3() per agent per frame _agentVisualsTemp.init(); const dir = _agentVisualsTemp._dir.subVectors(agent.taskState.targetPosition, agent.mesh.position); if (dir.length() > 0.5) { const targetAngle = Math.atan2(dir.x, dir.z); agent.mesh.rotation.y = THREE.MathUtils.lerp(agent.mesh.rotation.y, targetAngle, deltaTime * 5); } } } // v5.16: Run autonomous task behavior for an agent function runAgentAutonomousTask(agent, deltaTime) { const task = agent.taskState; const agentPos = agent.mesh.position; // Don't run tasks if alerted (waiting for player) if (task.alert && task.state === 'alert') { return; } // Decrement action cooldown if (task.actionCooldown > 0) { task.actionCooldown -= 100; return; } // Task behavior based on agent type switch (agent.type) { case 'gatherer': case 'miner': runGathererTask(agent); break; case 'hunter': case 'protector': runHunterTask(agent); break; case 'scout': case 'explorer': runScoutTask(agent); break; case 'healer': runHealerTask(agent); break; case 'fisher': runFisherTask(agent); break; case 'builder': // v6.66: RCT-style autonomous building if (typeof runBuilderTask === 'function') { runBuilderTask(agent); } break; case 'terraformer': // v6.66: RCT-style terrain modification if (typeof runTerraformerTask === 'function') { runTerraformerTask(agent); } break; default: runGathererTask(agent); // Default behavior } // Move towards target if we have one // v6.4.0: Also move when returning to ship if (task.targetPosition && (task.state === 'moving' || task.state === 'returning')) { moveAgentTowards(agent, task.targetPosition, deltaTime); } } // v6.4.0: Gatherer/Miner task - FULL HAULING CYCLE // 1. Gather resources until inventory full // 2. Return to ship to deposit // 3. Repeat function runGathererTask(agent) { const task = agent.taskState; const agentPos = agent.mesh.position; const isMiner = agent.type === 'miner'; // Initialize inventory if missing (backwards compatibility) if (!task.inventory) task.inventory = []; if (!task.carryingCapacity) task.carryingCapacity = 6 + Math.floor(agent.agentLevel / 2); // Update capacity based on level task.carryingCapacity = 6 + Math.floor(agent.agentLevel / 2); // ===== STATE: DEPOSITING ===== // At ship, depositing resources if (task.state === 'depositing') { depositAgentInventory(agent); return; } // ===== STATE: RETURNING ===== // Inventory full or returning - head to ship // v7.74: Use distanceToSquared for performance if (task.state === 'returning' || task.inventory.length >= task.carryingCapacity) { if (task.inventory.length === 0) { // Nothing to deposit, go back to gathering task.state = 'idle'; task.targetObject = null; task.targetPosition = null; return; } // Check if we're at the ship const shipPos = SHIP_STATE.position; const distSqToShip = agentPos.distanceToSquared(shipPos); if (distSqToShip < 25) { // 5*5=25 // At ship - deposit! task.state = 'depositing'; task.targetPosition = null; logAgentTask(agent, `Arrived at ship with ${task.inventory.length} items!`); agent.statusMessage = `📦 Depositing ${task.inventory.length} items...`; updateAgentCardUI(agent); return; } // Not at ship yet - move towards it task.state = 'returning'; // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, shipPos); task.targetObject = null; agent.statusMessage = `🚀 Returning to ship (${task.inventory.length}/${task.carryingCapacity})`; updateAgentCardUI(agent); return; } // ===== STATE: WORKING ===== // If we have a resource target, check if close enough to harvest // v7.74: Use distanceToSquared for performance if (task.targetObject && task.targetObject.parent) { const distSq = agentPos.distanceToSquared(task.targetObject.position); if (distSq < 4) { // 2*2=4 // Close enough to interact task.state = 'working'; performAgentHarvest(agent, task.targetObject); return; } else { // Move towards target task.state = 'moving'; // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, task.targetObject.position); agent.statusMessage = `🎯 Moving to ${task.targetObject.userData?.name || 'resource'}`; return; } } // ===== STATE: IDLE - Find new resource ===== // v8.09: forEach to for loop + InteractableSpatialGrid for O(1) nearby lookup let bestTarget = null; let bestDistSq = 1600; // 40*40=1600 - Increased search range squared // Use spatial grid with radius 5 cells (40 units / 8 cell size = 5) const resourceCandidates = InteractableSpatialGrid.getNearby(agentPos.x, agentPos.z, 5); for (let rci = 0, rclen = resourceCandidates.length; rci < rclen; rci++) { const obj = resourceCandidates[rci]; if (!obj.parent) continue; const name = obj.userData?.name || ''; const type = obj.userData?.type || ''; // Filter by agent type const isValidTarget = isMiner ? (type === 'rock' || name.includes('Ore') || name.includes('Crystal') || name.includes('Stone')) : (type === 'tree' || name.includes('Tree') || name.includes('Bush') || name.includes('Plant') || name.includes('Herb')); if (!isValidTarget) continue; const distSq = agentPos.distanceToSquared(obj.position); if (distSq < bestDistSq) { bestDistSq = distSq; bestTarget = obj; } } if (bestTarget) { task.targetObject = bestTarget; // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, bestTarget.position); task.state = 'moving'; const invStatus = task.inventory.length > 0 ? ` [${task.inventory.length}/${task.carryingCapacity}]` : ''; agent.statusMessage = `🔍 Found ${bestTarget.userData?.name || 'resource'}${invStatus}`; logAgentTask(agent, `Found ${bestTarget.userData?.name || 'resource'} at distance ${Math.floor(Math.sqrt(bestDistSq))}`); } else { // No resources found, wander // v7.90: Use setWanderTarget to avoid Vector3 allocation if (!task.targetPosition || agentPos.distanceToSquared(task.targetPosition) < 4) { // 2*2=4 setWanderTarget(task, agentPos); task.state = 'moving'; agent.statusMessage = `🔎 Searching for resources...`; logAgentTask(agent, 'No resources nearby, wandering...'); } } } // v6.4.0: Agent harvests resource and adds to AGENT inventory (not player) function performAgentHarvest(agent, target) { const task = agent.taskState; const data = target.userData; if (!data || data.hp === undefined) return; // Deal "damage" to resource const damage = 2 + Math.floor(agent.agentLevel / 3); data.hp -= damage; // Visual feedback target.scale.setScalar(0.9); setTimeout(() => { if (target.parent) target.scale.setScalar(1); }, 100); // Particles if (particles) { const particleColor = data.type === 'tree' ? 0x885522 : 0x888888; particles.emit(target.position, 3, particleColor, { spread: 1.5, lifetime: 400, size: 0.1 }); } // Swing animation for agent if (agent.mesh) { agent.mesh.rotation.y += 0.3; setTimeout(() => { if (agent.mesh) agent.mesh.rotation.y -= 0.3; }, 150); } // Check if destroyed if (data.hp <= 0) { let itemName = 'Resource'; let itemCount = 1 + Math.floor(agent.agentLevel / 4); if (data.type === 'tree') { itemName = Math.random() < 0.3 ? 'Fiber' : 'Log'; itemCount = 2 + Math.floor(agent.agentLevel / 3); } else if (data.type === 'rock') { const ores = ['Stone', 'Iron Ore', 'Copper Ore']; if (agent.agentLevel >= 3) ores.push('Silver Ore'); if (agent.agentLevel >= 5) ores.push('Gold Ore'); itemName = ores[Math.floor(Math.random() * ores.length)]; itemCount = 2 + Math.floor(agent.agentLevel / 3); } else if (data.name?.includes('Herb') || data.name?.includes('Plant')) { itemName = 'Herbs'; itemCount = 1 + Math.floor(agent.agentLevel / 4); } // Add to AGENT inventory (not player!) for (let i = 0; i < itemCount; i++) { if (task.inventory.length < task.carryingCapacity) { task.inventory.push(itemName); } } // Track earnings agent.totalEarnings.items.push({ item: itemName, amount: itemCount }); // Visual/audio feedback spawnFloater(target.position, `+${itemCount} ${itemName}`, '#ffdd00'); spawnFloater(agent.mesh.position, `📦 ${task.inventory.length}/${task.carryingCapacity}`, '#0ff'); AudioSystem.collect(); // Update agent status const invFull = task.inventory.length >= task.carryingCapacity; agent.statusMessage = invFull ? `📦 Inventory FULL! Returning to ship...` : `Got ${itemCount} ${itemName}! [${task.inventory.length}/${task.carryingCapacity}]`; agent.progress = Math.min(100, agent.progress + 5); logAgentTask(agent, `Harvested ${itemCount} ${itemName} (carrying ${task.inventory.length}/${task.carryingCapacity})`); trackAgentAction(agent, true, itemCount * 2); updateAgentCardUI(agent); // Remove from world scene.remove(target); worldState.interactables = worldState.interactables.filter(x => x !== target); // Clear target task.targetObject = null; task.targetPosition = null; task.state = 'idle'; // If full, trigger return if (invFull) { task.state = 'returning'; } } task.actionCooldown = 400; // Slightly faster than before } // v6.4.0: Agent deposits inventory at ship function depositAgentInventory(agent) { const task = agent.taskState; if (task.inventory.length === 0) { task.state = 'idle'; agent.statusMessage = 'Inventory empty, returning to work!'; updateAgentCardUI(agent); return; } // Deposit one item per tick (animated feel) const item = task.inventory.shift(); // Add to player inventory addToInventory(item, 1); // Track stats task.totalHauled = (task.totalHauled || 0) + 1; // Visual feedback at ship if (SHIP_STATE.mesh) { const shipPos = SHIP_STATE.position; spawnFloater(new THREE.Vector3(shipPos.x, shipPos.y + 2, shipPos.z), `+1 ${item}`, '#00ff88'); } // Update status if (task.inventory.length > 0) { agent.statusMessage = `📦 Depositing... (${task.inventory.length} left)`; } else { // All deposited! task.tripsCompleted = (task.tripsCompleted || 0) + 1; const totalItems = task.totalHauled || 0; agent.statusMessage = `✅ Delivery complete! Trip #${task.tripsCompleted}`; logAgentTask(agent, `Completed delivery #${task.tripsCompleted} (${totalItems} total items hauled)`); // Bonus XP for completing a trip const tripXP = 10 + task.tripsCompleted * 2; grantAgentXP(agent, tripXP); // Celebrate! if (particles && agent.mesh) { particles.emit(agent.mesh.position, 10, 0x00ff88, { spread: 2, lifetime: 800, size: 0.15 }); } spawnFloater(agent.mesh.position, `🎉 Trip #${task.tripsCompleted}!`, '#ffd700'); // Return to gathering after short delay task.state = 'idle'; task.actionCooldown = 500; } updateAgentCardUI(agent); task.actionCooldown = 200; // Fast deposit animation } // v6.4.0: Grant XP to agent (for trip completion bonuses etc) function grantAgentXP(agent, xp) { if (typeof trackAgentAction === 'function') { // Use existing XP system agent.agentXP = (agent.agentXP || 0) + xp; const xpNeeded = getAgentXPForLevel(agent.agentLevel); if (agent.agentXP >= xpNeeded) { agent.agentLevel++; agent.agentXP -= xpNeeded; spawnFloater(agent.mesh?.position || agent.position, `⬆️ Level ${agent.agentLevel}!`, '#ffd700'); logAgentTask(agent, `Leveled up to ${agent.agentLevel}!`); } } } // v5.16: Hunter task - find and attack enemies // v7.74: Use distanceToSquared for performance function runHunterTask(agent) { const task = agent.taskState; const agentPos = agent.mesh.position; // Check agent HP - alert if low if (task.hp < task.maxHp * 0.3 && !task.alert) { triggerAgentAlert(agent, 'Low HP! Need healing assistance.'); return; } // If we have a target enemy, attack it if (task.targetObject && task.targetObject.parent && task.targetObject.userData?.hp > 0) { const distSq = agentPos.distanceToSquared(task.targetObject.position); if (distSq < 6.25) { // 2.5*2.5=6.25 task.state = 'combat'; performAgentCombat(agent, task.targetObject); return; } else { task.state = 'moving'; // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, task.targetObject.position); return; } } // Find a new enemy target let bestTarget = null; let bestDistSq = 625; // 25*25=625 // v8.04: forEach to for loop conversion (protector AI) const protectMobs = worldState.mobs; for (let pi = 0, plen = protectMobs.length; pi < plen; pi++) { const mob = protectMobs[pi]; if (!mob.mesh || !mob.mesh.parent) continue; if (mob.userData?.hp <= 0) continue; // Avoid bosses unless protector const isBoss = mob.userData?.type === 'boss' || mob.userData?.isBoss; if (isBoss && agent.type !== 'protector') { // Alert about boss! if (!task.alert) { triggerAgentAlert(agent, `Found BOSS: ${mob.userData?.name || 'Unknown'}! Need backup!`); } continue; } const distSq = agentPos.distanceToSquared(mob.mesh.position); if (distSq < bestDistSq) { bestDistSq = distSq; bestTarget = mob; } } if (bestTarget) { task.targetObject = bestTarget.mesh; // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, bestTarget.mesh.position); task.state = 'moving'; logAgentTask(agent, `Engaging ${bestTarget.userData?.name || 'enemy'}`); } else { // No enemies, patrol near player if (!task.targetPosition || agentPos.distanceToSquared(task.targetPosition) < 4) { // 2*2=4 if (worldState.player) { const offset = new THREE.Vector3( (Math.random() - 0.5) * 15, 0, (Math.random() - 0.5) * 15 ); // v7.86: Use setAgentTargetWithOffset instead of clone().add() setAgentTargetWithOffset(task, worldState.player.position, offset); } else { task.targetPosition = getRandomWanderPosition(agentPos); } task.state = 'moving'; } } } // v5.16: Scout/Explorer task - explore and discover // v7.74: Use distanceToSquared for performance function runScoutTask(agent) { const task = agent.taskState; const agentPos = agent.mesh.position; // Scouts move faster and explore wider if (!task.targetPosition || agentPos.distanceToSquared(task.targetPosition) < 9) { // 3*3=9 // Pick a distant unexplored area const angle = Math.random() * Math.PI * 2; const distance = 20 + Math.random() * 25; task.targetPosition = new THREE.Vector3( agentPos.x + Math.cos(angle) * distance, 0, agentPos.z + Math.sin(angle) * distance ); // Clamp to world bounds task.targetPosition.x = Math.max(-45, Math.min(45, task.targetPosition.x)); task.targetPosition.z = Math.max(-45, Math.min(45, task.targetPosition.z)); task.state = 'moving'; logAgentTask(agent, `Scouting towards (${Math.floor(task.targetPosition.x)}, ${Math.floor(task.targetPosition.z)})`); } // Report nearby discoveries // v8.09: forEach to for loop + InteractableSpatialGrid const nearbyPOIs = InteractableSpatialGrid.getNearby(agentPos.x, agentPos.z, 1); for (let pi = 0, plen = nearbyPOIs.length; pi < plen; pi++) { const obj = nearbyPOIs[pi]; if (!obj.parent) continue; const distSq = agentPos.distanceToSquared(obj.position); if (distSq < 25 && obj.userData?.type === 'poi' && !obj.userData?.discovered) { // 5*5=25 logAgentTask(agent, `Discovered POI: ${obj.userData?.name || 'Unknown'}`); agent.statusMessage = `Found ${obj.userData?.name}!`; updateAgentCardUI(agent); } } } // v5.16: Healer task - follow player and heal // v7.74: Use distanceToSquared for performance function runHealerTask(agent) { const task = agent.taskState; const agentPos = agent.mesh.position; // Check if player needs healing if (gameData.player && gameData.player.hp < gameData.player.maxHp * 0.7) { if (worldState.player) { const distSqToPlayer = agentPos.distanceToSquared(worldState.player.position); if (distSqToPlayer < 9) { // 3*3=9 // Heal player task.state = 'working'; const healAmount = 5; gameData.player.hp = Math.min(gameData.player.maxHp, gameData.player.hp + healAmount); spawnFloater(worldState.player.position, `+${healAmount} HP`, '#44ff44'); updateHealthUI(); task.actionCooldown = 2000; // 2 second cooldown logAgentTask(agent, `Healed player for ${healAmount} HP`); agent.statusMessage = 'Healing player...'; updateAgentCardUI(agent); return; } else { // Move to player // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, worldState.player.position); task.state = 'moving'; return; } } } // Check if any other agent needs healing for (const otherAgent of agentFleet) { if (otherAgent.id === agent.id) continue; if (!otherAgent.taskState || otherAgent.taskState.hp >= otherAgent.taskState.maxHp) continue; const distSq = agentPos.distanceToSquared(otherAgent.mesh.position); if (distSq < 9) { // 3*3=9 otherAgent.taskState.hp = Math.min(otherAgent.taskState.maxHp, otherAgent.taskState.hp + 10); spawnFloater(otherAgent.mesh.position, '+10 HP', '#44ff44'); task.actionCooldown = 2000; logAgentTask(agent, `Healed ${otherAgent.name}`); return; } else if (distSq < 400) { // 20*20=400 // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, otherAgent.mesh.position); task.state = 'moving'; return; } } // No one needs healing, follow player loosely if (worldState.player) { const distSqToPlayer = agentPos.distanceToSquared(worldState.player.position); if (distSqToPlayer > 64) { // 8*8=64 // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, worldState.player.position); task.state = 'moving'; } } } // v5.16: Fisher task - find and use fishing spots // v7.74: Use distanceToSquared for performance function runFisherTask(agent) { const task = agent.taskState; const agentPos = agent.mesh.position; // Find fishing spot if (worldState.fishingSpots) { let bestSpot = null; let bestDistSq = 2500; // 50*50=2500 worldState.fishingSpots.forEach(spot => { if (!spot.parent) return; const distSq = agentPos.distanceToSquared(spot.position); if (distSq < bestDistSq) { bestDistSq = distSq; bestSpot = spot; } }); if (bestSpot) { if (bestDistSq < 4) { // 2*2=4 // Fish! task.state = 'working'; if (Math.random() < 0.1) { // 10% chance per tick const fishTypes = ['Small Fish', 'Medium Fish', 'Large Fish']; const fish = fishTypes[Math.floor(Math.random() * fishTypes.length)]; addItem(fish); agent.totalEarnings.items.push(fish); spawnFloater(agentPos, `+1 ${fish}`, '#4488ff'); logAgentTask(agent, `Caught ${fish}!`); agent.statusMessage = `Caught ${fish}!`; updateAgentCardUI(agent); } task.actionCooldown = 500; return; } else { // v7.86: Use setAgentTarget instead of clone() setAgentTarget(task, bestSpot.position); task.state = 'moving'; return; } } } // No fishing spots, wander // v7.80: distanceToSquared optimization // v7.90: Use setWanderTarget to avoid Vector3 allocation if (!task.targetPosition || agentPos.distanceToSquared(task.targetPosition) < 4) { // 2*2=4 setWanderTarget(task, agentPos); task.state = 'moving'; } } // v7.90: Pre-allocated temp vector for moveAgentTowards (high-frequency hot path) let _moveAgentDir = null; // v5.16: Move agent towards a position // v7.90: Optimized to use pre-allocated direction vector function moveAgentTowards(agent, targetPos, deltaTime) { if (!_moveAgentDir) _moveAgentDir = new THREE.Vector3(); const direction = _moveAgentDir.subVectors(targetPos, agent.mesh.position); direction.y = 0; if (direction.length() > 0.3) { direction.normalize(); const speed = agent.type === 'scout' || agent.type === 'explorer' ? 4 : 3; agent.mesh.position.x += direction.x * deltaTime * speed; agent.mesh.position.z += direction.z * deltaTime * speed; // v6.5.1: Snap agent to terrain height as they move if (typeof getTerrainHeight === 'function') { agent.mesh.position.y = getTerrainHeight(agent.mesh.position.x, agent.mesh.position.z); } agent.position.copy(agent.mesh.position); } } // v7.90: Pre-allocated wander position calculation temp vector let _wanderCalcTemp = null; // v5.16: Get random wander position // v7.90: Changed to setWanderTarget() pattern - sets task._targetVec directly // This avoids creating new Vector3 on each call while being safe for multiple agents function setWanderTarget(task, currentPos) { if (!_wanderCalcTemp) _wanderCalcTemp = new THREE.Vector3(); const angle = Math.random() * Math.PI * 2; const distance = 8 + Math.random() * 12; _wanderCalcTemp.set( Math.max(-45, Math.min(45, currentPos.x + Math.cos(angle) * distance)), 0, Math.max(-45, Math.min(45, currentPos.z + Math.sin(angle) * distance)) ); // Use setAgentTarget to properly copy into task's pre-allocated vector setAgentTarget(task, _wanderCalcTemp); } // v5.16: Get random wander position (legacy - still allocates for compatibility) // Prefer setWanderTarget() for hot paths function getRandomWanderPosition(currentPos) { const angle = Math.random() * Math.PI * 2; const distance = 8 + Math.random() * 12; const pos = new THREE.Vector3( currentPos.x + Math.cos(angle) * distance, 0, currentPos.z + Math.sin(angle) * distance ); pos.x = Math.max(-45, Math.min(45, pos.x)); pos.z = Math.max(-45, Math.min(45, pos.z)); return pos; } // v5.16: Agent performs action on target (gathering) function performAgentAction(agent, target) { const task = agent.taskState; const data = target.userData; if (!data || data.hp === undefined) return; // Deal "damage" to resource const damage = 2; data.hp -= damage; // Visual feedback target.scale.setScalar(0.9); setTimeout(() => { if (target.parent) target.scale.setScalar(1); }, 100); // Particles if (particles) { const particleColor = data.type === 'tree' ? 0x885522 : 0x888888; particles.emit(target.position, 3, particleColor, { spread: 1.5, lifetime: 400, size: 0.1 }); } // Check if destroyed if (data.hp <= 0) { let itemName = 'Resource'; let itemCount = 1; if (data.type === 'tree') { itemName = 'Log'; itemCount = 2; } else if (data.type === 'rock') { itemName = 'Ore'; itemCount = 2; } for (let i = 0; i < itemCount; i++) addItem(itemName); agent.totalEarnings.items.push(itemName); spawnFloater(target.position, `+${itemCount} ${itemName}`, '#ffdd00'); AudioSystem.collect(); logAgentTask(agent, `Harvested ${itemCount} ${itemName}`); agent.statusMessage = `Got ${itemCount} ${itemName}!`; agent.progress = Math.min(100, agent.progress + 5); updateAgentCardUI(agent); // Remove from world scene.remove(target); worldState.interactables = worldState.interactables.filter(x => x !== target); // Clear target task.targetObject = null; task.targetPosition = null; task.state = 'idle'; } task.actionCooldown = 500; // Half second between hits } // v5.16: Agent performs combat function performAgentCombat(agent, targetMesh) { const task = agent.taskState; const data = targetMesh.userData; if (!data || data.hp === undefined || data.hp <= 0) { task.targetObject = null; task.state = 'idle'; return; } // Deal damage const damage = 8; data.hp -= damage; spawnFloater(targetMesh.position, `-${damage}`, '#ff4444'); // Visual feedback targetMesh.scale.setScalar(0.85); setTimeout(() => { if (targetMesh.parent) targetMesh.scale.setScalar(1); }, 100); // Particles if (particles) { particles.emit(targetMesh.position, 5, 0xff4444, { spread: 2, lifetime: 500 }); } // Agent takes damage from enemy const enemyDamage = Math.floor(Math.random() * 5) + 2; task.hp -= enemyDamage; // Check if agent died if (task.hp <= 0) { triggerAgentAlert(agent, 'Agent down! Need revival!'); task.hp = 1; // Keep alive but alert task.state = 'alert'; return; } // Check if enemy killed if (data.hp <= 0) { const xpReward = data.xp || 20; const goldReward = Math.floor(Math.random() * 10) + 5; addXp('combat', xpReward); agent.totalEarnings.xp += xpReward; agent.totalEarnings.gold += goldReward; spawnFloater(targetMesh.position, `+${xpReward} XP`, '#ffff00'); AudioSystem.levelUp(); logAgentTask(agent, `Defeated ${data.name || 'enemy'}! +${xpReward} XP`); agent.statusMessage = `Killed ${data.name}!`; agent.progress = Math.min(100, agent.progress + 10); updateAgentCardUI(agent); // Remove mob scene.remove(targetMesh); worldState.mobs = worldState.mobs.filter(m => m.mesh !== targetMesh); task.targetObject = null; task.state = 'idle'; } task.actionCooldown = 800; } // v5.16: Trigger agent alert function triggerAgentAlert(agent, message) { const task = agent.taskState; task.alert = message; task.state = 'alert'; logAgentTask(agent, `ALERT: ${message}`); agent.statusMessage = `NEEDS HELP: ${message}`; updateAgentCardUI(agent); // Notify player addCopilotMessage(`⚠️ ${agent.typeConfig.icon} ${agent.name} needs help: ${message}`, 'ai'); // Visual: make alert indicator visible if (agent.alertIndicator) { agent.alertIndicator.material.color.setHex(0xff0000); } } // v5.16: Log agent task action (for troubleshooting) function logAgentTask(agent, message) { if (!agent.taskState) return; const log = agent.taskState.taskLog; log.push({ time: new Date().toLocaleTimeString(), message: message }); // Keep last 20 log entries if (log.length > 20) log.shift(); } // v5.16: Check if player is near an alerted agent (for troubleshooting) // v7.80: distanceToSquared optimization // v8.06: Converted forEach to for loop function checkAgentTroubleshooting() { if (!worldState.player) return; for (let i = 0, len = agentFleet.length; i < len; i++) { const agent = agentFleet[i]; if (!agent.mesh || !agent.taskState?.alert) continue; const distSq = worldState.player.position.distanceToSquared(agent.mesh.position); if (distSq < 9) { // 3*3=9 // Player is close to alerted agent - show troubleshooting UI showAgentTroubleshootingUI(agent); } } } // v5.16: Show troubleshooting UI for agent function showAgentTroubleshootingUI(agent) { // Create or update troubleshooting tooltip let tooltip = document.getElementById('agent-troubleshoot-tooltip'); if (!tooltip) { tooltip = document.createElement('div'); tooltip.id = 'agent-troubleshoot-tooltip'; tooltip.style.cssText = ` position: fixed; bottom: 200px; left: 50%; transform: translateX(-50%); background: rgba(10, 20, 30, 0.95); padding: 15px; border-radius: 10px; border: 2px solid #ff4444; max-width: 400px; z-index: 1000; font-size: 12px; color: #fff; backdrop-filter: blur(5px); `; document.body.appendChild(tooltip); } const task = agent.taskState; const logs = task.taskLog.slice(-5).reverse(); tooltip.innerHTML = `
⚠️ ${agent.typeConfig.icon} ${agent.name} - NEEDS HELP
${task.alert}
Agent HP
${task.hp}/${task.maxHp}
State
${task.state}
Recent Activity:
${logs.map(l => `
${l.time} ${l.message}
`).join('')}
`; tooltip.style.display = 'block'; // Auto-hide when player moves away // v7.80: distanceToSquared optimization setTimeout(() => { if (worldState.player && agent.mesh) { const distSq = worldState.player.position.distanceToSquared(agent.mesh.position); if (distSq > 25) { // 5*5=25 tooltip.style.display = 'none'; } } }, 500); } // v5.16: Dismiss agent alert function dismissAgentAlert(agentId) { const agent = agentLookup.get(agentId); if (agent && agent.taskState) { agent.taskState.alert = null; agent.taskState.state = 'idle'; agent.statusMessage = 'Alert dismissed, resuming...'; updateAgentCardUI(agent); } const tooltip = document.getElementById('agent-troubleshoot-tooltip'); if (tooltip) tooltip.style.display = 'none'; } // v5.16: Heal agent from player function healAgentFromPlayer(agentId) { const agent = agentLookup.get(agentId); if (agent && agent.taskState) { agent.taskState.hp = agent.taskState.maxHp; agent.taskState.alert = null; agent.taskState.state = 'idle'; spawnFloater(agent.mesh.position, 'FULLY HEALED', '#44ff44'); agent.statusMessage = 'Healed and ready!'; updateAgentCardUI(agent); logAgentTask(agent, 'Healed by player'); } const tooltip = document.getElementById('agent-troubleshoot-tooltip'); if (tooltip) tooltip.style.display = 'none'; } // v5.16: Reset agent task function resetAgentTask(agentId) { const agent = agentLookup.get(agentId); if (agent && agent.taskState) { agent.taskState.targetObject = null; agent.taskState.targetPosition = null; agent.taskState.alert = null; agent.taskState.state = 'idle'; agent.taskState.stuckCounter = 0; agent.statusMessage = 'Task reset, finding new objective...'; updateAgentCardUI(agent); logAgentTask(agent, 'Task reset by player'); } const tooltip = document.getElementById('agent-troubleshoot-tooltip'); if (tooltip) tooltip.style.display = 'none'; } // Parse natural language commands for agent fleet function parseAgentFleetCommand(message) { const lowerMsg = message.toLowerCase(); // Spawn commands const spawnPatterns = [ { pattern: /spawn\s+(a\s+)?(\d+\s+)?gatherer/i, type: 'gatherer' }, { pattern: /spawn\s+(a\s+)?(\d+\s+)?hunter/i, type: 'hunter' }, { pattern: /spawn\s+(a\s+)?(\d+\s+)?scout/i, type: 'scout' }, { pattern: /spawn\s+(a\s+)?(\d+\s+)?protector/i, type: 'protector' }, { pattern: /spawn\s+(a\s+)?(\d+\s+)?healer/i, type: 'healer' }, { pattern: /spawn\s+(a\s+)?(\d+\s+)?fisher/i, type: 'fisher' }, { pattern: /spawn\s+(a\s+)?(\d+\s+)?miner/i, type: 'miner' }, { pattern: /spawn\s+(a\s+)?(\d+\s+)?explorer/i, type: 'explorer' }, { pattern: /send\s+(a\s+)?(\d+\s+)?agent/i, type: null }, { pattern: /deploy\s+(a\s+)?(\d+\s+)?agent/i, type: null }, ]; for (const { pattern, type } of spawnPatterns) { const match = lowerMsg.match(pattern); if (match) { if (type) { const count = parseInt(match[2]) || 1; for (let i = 0; i < Math.min(count, MAX_AGENTS - agentFleet.length); i++) { spawnAgent(type); } return true; } else { // Generic spawn - suggest opening the fleet panel toggleAgentFleetPanel(); addCopilotMessage(`Fleet panel opened! Select an agent type to spawn.`, 'ai'); return true; } } } // Alternative spawn phrases if (lowerMsg.includes('send agent') || lowerMsg.includes('send out') || lowerMsg.includes('deploy agent')) { toggleAgentFleetPanel(); addCopilotMessage(`Fleet panel opened! Choose an agent type to deploy.`, 'ai'); return true; } // Recall all agents if (lowerMsg.includes('recall all') || lowerMsg.includes('bring back all') || lowerMsg.includes('return all agents')) { if (agentFleet.length > 0) { const count = agentFleet.length; [...agentFleet].forEach(agent => recallAgent(agent.id)); addCopilotMessage(`Recalled all ${count} agents!`, 'ai'); return true; } else { addCopilotMessage(`No agents are currently deployed.`, 'ai'); return true; } } // Fleet status if (lowerMsg.includes('fleet status') || lowerMsg.includes('agent status') || lowerMsg.includes('how many agents')) { if (agentFleet.length === 0) { addCopilotMessage(`No agents deployed. Open the fleet panel (🤖 button) to spawn agents!`, 'ai'); } else { const summary = agentFleet.map(a => `${a.typeConfig.icon} ${a.name} (${a.typeConfig.name})`).join(', '); addCopilotMessage(`${agentFleet.length}/${MAX_AGENTS} agents deployed: ${summary}`, 'ai'); } return true; } // Open fleet panel if (lowerMsg.includes('open fleet') || lowerMsg.includes('show agents') || lowerMsg.includes('agent panel')) { toggleAgentFleetPanel(); return true; } return false; } // ============================================ // v5.10: TRANSCRIPT EXPORT SYSTEM // Export agent transcripts for debugging // ============================================ let currentTranscriptAgentId = null; // v5.12.1: Build a standard transcript JSON for an agent (includes endpoint config) function buildAgentTranscript(agent) { const elapsed = (performance.now() - agent.spawnTime) / 1000; const agentEndpoint = getAgentEndpoint(agent); return { transcript_version: "1.1", export_timestamp: new Date().toISOString(), application: "LEVIATHAN: OMNIVERSE", application_version: VERSION, agent: { id: agent.id, name: agent.name, type: agent.type, type_config: { icon: agent.typeConfig.icon, name: agent.typeConfig.name, decision_interval_ms: agent.typeConfig.decisionInterval, task_type: agent.typeConfig.taskType }, status: agent.status, status_message: agent.statusMessage, progress_percent: Math.round(agent.progress), spawn_time: new Date(Date.now() - elapsed * 1000).toISOString(), runtime_seconds: Math.floor(elapsed) }, // v5.12.1: Endpoint configuration for spawning agents with different AI providers // v5.14: Now includes profile reference endpoint_config: { name: agentEndpoint.name, profile_id: agent.profileId || null, profile_name: agent.profileId ? getEndpointProfile(agent.profileId)?.name : null, url: agent.endpointConfig?.url || agentEndpoint.url || '${URL_PLACEHOLDER}', // v5.15: Check all possible key sources (profile, config, or global fallback) apiKey: (agent.endpointConfig?.apiKey || agent.profileId || agentEndpoint.key) ? '${API_KEY_PLACEHOLDER}' : null, urlKey: agent.endpointConfig?.urlKey || null, apiKeyKey: agent.endpointConfig?.apiKeyKey || null, headerStyle: agentEndpoint.headerStyle, headerPrefix: agentEndpoint.headerPrefix || '', bodyFormat: agentEndpoint.bodyFormat, model: agentEndpoint.model || agent.endpointConfig?.model || null, active_endpoint: agent.activeEndpoint || 'default' }, game_context: { player_hp: gameData.player?.hp || 100, player_max_hp: gameData.player?.maxHp || 100, current_biome: worldState?.currentCiv?.biomeName || 'Unknown', planet_name: worldState?.currentCiv?.name || 'Unknown', player_position: worldState.player ? { x: Math.floor(worldState.player.position.x), z: Math.floor(worldState.player.position.z) } : null }, earnings: { total_xp: agent.totalEarnings.xp, total_gold: agent.totalEarnings.gold, items_collected: agent.totalEarnings.items }, results_log: agent.results, conversation_history: agent.conversationHistory.map((msg, idx) => ({ index: idx, role: msg.role, content: msg.content, // Include endpoint in first message for transcript re-import endpoint: idx === 0 && agent.endpointConfig ? { url: agent.endpointConfig.url || '${URL_PLACEHOLDER}', apiKey: '${API_KEY_PLACEHOLDER}', headerStyle: agent.endpointConfig.headerStyle, bodyFormat: agent.endpointConfig.bodyFormat, model: agent.endpointConfig.model } : undefined, timestamp: null })), system_prompt: agent.conversationHistory.length > 0 ? agent.conversationHistory[0].content : null, total_messages: agent.conversationHistory.length, api_endpoint: agentEndpoint.url || 'local-simulation' }; } // Build transcript for all agents function buildAllAgentTranscripts() { // v5.15: Include ship defense statistics const defenseStats = getDefenseStats(); return { transcript_version: "1.1", export_timestamp: new Date().toISOString(), application: "LEVIATHAN: OMNIVERSE", application_version: VERSION, fleet_summary: { total_agents: agentFleet.length, max_agents: MAX_AGENTS, agent_types: agentFleet.reduce((acc, a) => { acc[a.type] = (acc[a.type] || 0) + 1; return acc; }, {}), total_xp_earned: agentFleet.reduce((sum, a) => sum + a.totalEarnings.xp, 0), total_gold_earned: agentFleet.reduce((sum, a) => sum + a.totalEarnings.gold, 0), total_items_collected: agentFleet.reduce((sum, a) => sum + a.totalEarnings.items.length, 0) }, // v5.15: Ship defense summary ship_defense: { hull_hp: SHIP_STATE.currentHP, hull_max_hp: SHIP_STATE.maxHP, auto_defense_enabled: SHIP_STATE.autoDefend, statistics: { total_engagements: defenseStats.engagements, total_kills: defenseStats.kills, total_damage_dealt: defenseStats.damageDealt, entities_deterred: defenseStats.deterred, times_attacked: defenseStats.attacked, total_damage_taken: defenseStats.damageTaken, kill_rate_percent: defenseStats.killRatio, repairs_performed: defenseStats.repairs, repair_costs_total: defenseStats.repairCost, times_destroyed: defenseStats.destroyed }, recent_events: SHIP_STATE.defenseLog.events.slice(-20).map(e => ({ type: e.type, time: e.time, details: { ...e, type: undefined, timestamp: undefined, time: undefined } })) }, game_context: { player_hp: gameData.player?.hp || 100, player_max_hp: gameData.player?.maxHp || 100, current_biome: worldState?.currentCiv?.biomeName || 'Unknown', planet_name: worldState?.currentCiv?.name || 'Unknown' }, agents: agentFleet.map(agent => buildAgentTranscript(agent)), copilot_conversation_history: copilotConversationHistory }; } // Open transcript viewer modal function openTranscriptViewer() { const modal = document.getElementById('transcript-modal'); modal.classList.add('active'); // Build tabs const tabsContainer = document.getElementById('transcript-tabs'); let tabsHtml = ``; agentFleet.forEach(agent => { tabsHtml += ``; }); // Add copilot main chat tabsHtml += ``; tabsContainer.innerHTML = tabsHtml; // Show all agents by default selectTranscriptTab('all'); } // Close transcript viewer function closeTranscriptViewer() { document.getElementById('transcript-modal').classList.remove('active'); } // Select a transcript tab function selectTranscriptTab(agentId) { currentTranscriptAgentId = agentId; // Update tab active state document.querySelectorAll('.transcript-tab').forEach(tab => tab.classList.remove('active')); event.target.classList.add('active'); const infoContainer = document.getElementById('transcript-agent-info'); const jsonView = document.getElementById('transcript-json-view'); if (agentId === 'all') { // Show all agents summary const transcript = buildAllAgentTranscripts(); infoContainer.innerHTML = `
Total Agents
${transcript.fleet_summary.total_agents}/${MAX_AGENTS}
Total XP
${transcript.fleet_summary.total_xp_earned}
Total Gold
${transcript.fleet_summary.total_gold_earned}
Items Collected
${transcript.fleet_summary.total_items_collected}
`; jsonView.innerHTML = syntaxHighlightJSON(JSON.stringify(transcript, null, 2)); } else if (agentId === 'copilot') { // Show main copilot conversation const transcript = { transcript_version: "1.0", export_timestamp: new Date().toISOString(), application: "LEVIATHAN: OMNIVERSE", type: "main_copilot", conversation_history: copilotConversationHistory.map((msg, idx) => ({ index: idx, role: msg.role, content: msg.content })), total_messages: copilotConversationHistory.length, rappid_settings: { enabled: rappidSettings.rappid, endpoint: getActiveEndpoint()?.name || 'none', tts_enabled: !!rappidSettings.azureTTSKey } }; infoContainer.innerHTML = `
Type
Main Copilot
Messages
${copilotConversationHistory.length}
RAPPID
${rappidSettings.rappid ? 'Enabled' : 'Disabled'}
`; jsonView.innerHTML = syntaxHighlightJSON(JSON.stringify(transcript, null, 2)); } else { // Show specific agent const agent = agentLookup.get(agentId); if (!agent) { jsonView.innerHTML = 'Agent not found'; return; } const transcript = buildAgentTranscript(agent); const elapsed = Math.floor((performance.now() - agent.spawnTime) / 1000); infoContainer.innerHTML = `
Agent
${agent.typeConfig.icon} ${agent.name}
Type
${agent.typeConfig.name}
Status
${agent.status}
Runtime
${elapsed}s
Messages
${agent.conversationHistory.length}
XP Earned
${agent.totalEarnings.xp}
`; jsonView.innerHTML = syntaxHighlightJSON(JSON.stringify(transcript, null, 2)); } } // Syntax highlight JSON for display function syntaxHighlightJSON(json) { return json .replace(/&/g, '&') .replace(//g, '>') .replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => { let cls = 'number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; } } else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; } return `${match}`; }); } // Copy current transcript to clipboard function copyTranscriptToClipboard() { let transcript; if (currentTranscriptAgentId === 'all') { transcript = buildAllAgentTranscripts(); } else if (currentTranscriptAgentId === 'copilot') { transcript = { transcript_version: "1.0", export_timestamp: new Date().toISOString(), application: "LEVIATHAN: OMNIVERSE", type: "main_copilot", conversation_history: copilotConversationHistory }; } else { const agent = agentLookup.get(currentTranscriptAgentId); if (!agent) return; transcript = buildAgentTranscript(agent); } navigator.clipboard.writeText(JSON.stringify(transcript, null, 2)).then(() => { showNotification('Transcript copied to clipboard!', 'success'); }).catch(err => { console.error('Copy failed:', err); showNotification('Failed to copy transcript', 'error'); }); } // Download current transcript as JSON file function downloadCurrentTranscript() { let transcript; let filename; if (currentTranscriptAgentId === 'all') { transcript = buildAllAgentTranscripts(); filename = `leviathan-fleet-transcript-${new Date().toISOString().split('T')[0]}.json`; } else if (currentTranscriptAgentId === 'copilot') { transcript = { transcript_version: "1.0", export_timestamp: new Date().toISOString(), application: "LEVIATHAN: OMNIVERSE", type: "main_copilot", conversation_history: copilotConversationHistory }; filename = `leviathan-copilot-transcript-${new Date().toISOString().split('T')[0]}.json`; } else { const agent = agentLookup.get(currentTranscriptAgentId); if (!agent) return; transcript = buildAgentTranscript(agent); filename = `leviathan-agent-${agent.name.toLowerCase()}-transcript-${new Date().toISOString().split('T')[0]}.json`; } downloadJSON(transcript, filename); } // Download all transcripts as a single JSON file function downloadAllTranscripts() { const transcript = buildAllAgentTranscripts(); const filename = `leviathan-full-fleet-export-${new Date().toISOString().split('T')[0]}.json`; downloadJSON(transcript, filename); } // Helper to download JSON function downloadJSON(data, filename) { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.click(); URL.revokeObjectURL(url); showNotification(`Downloaded: ${filename}`, 'success'); } // Export single agent transcript (for agent card button) function exportAgentTranscript(agentId) { const agent = agentLookup.get(agentId); if (!agent) return; const transcript = buildAgentTranscript(agent); const filename = `leviathan-agent-${agent.name.toLowerCase()}-transcript-${new Date().toISOString().split('T')[0]}.json`; downloadJSON(transcript, filename); } // Import transcript file (for replaying/debugging) function importTranscriptFile(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = function(e) { try { // v8.29: Use ErrorRecovery.safeJSONParse for safer parsing const transcript = ErrorRecovery.safeJSONParse(e.target.result, null); if (!transcript) { showNotification('Failed to parse transcript file', 'error'); return; } console.log('Imported transcript:', transcript); // Validate transcript structure if (!transcript.transcript_version) { showNotification('Invalid transcript format', 'error'); return; } // Show in console for debugging console.log('=== IMPORTED TRANSCRIPT ==='); console.log('Version:', transcript.transcript_version); console.log('Application:', transcript.application); if (transcript.agents) { console.log('Fleet with', transcript.agents.length, 'agents'); transcript.agents.forEach(agent => { // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(` - ${agent.agent.name} (${agent.agent.type}): ${agent.total_messages} messages`); }); } else if (transcript.agent) { console.log('Single agent:', transcript.agent.name); console.log('Messages:', transcript.total_messages); } else if (transcript.type === 'main_copilot') { console.log('Main copilot conversation'); console.log('Messages:', transcript.conversation_history?.length); } showNotification('Transcript imported - check console for details', 'success'); // Open transcript viewer with imported data // (Could add more functionality here to replay transcripts) } catch (error) { console.error('Import failed:', error); showNotification('Failed to parse transcript file', 'error'); } }; reader.readAsText(file); event.target.value = ''; // Reset input } // Context-aware responses based on game state const COPILOT_RESPONSES = { greeting: [ "Hello, Explorer! Ready for adventure?", "Greetings! I'm here to help you on your journey.", "Welcome back! What shall we explore today?" ], lowHealth: [ "Careful! Your health is low. Consider using a health potion or retreating.", "You're wounded! Look for healing items or rest at a safe spot.", "Warning: Low HP! Maybe craft some health potions?" ], nearEnemy: [ "Enemy spotted nearby! Prepare for combat.", "Be cautious, there's a hostile creature close by.", "I sense danger ahead. Ready your weapon!" ], afterKill: [ "Well done! That was impressive combat.", "Excellent work, Explorer!", "Another victory! Your skills are improving." ], exploration: [ "This area looks interesting. Let's explore!", "I wonder what secrets this place holds...", "Keep your eyes open for resources and treasures." ], tips: [ "Tip: Use WASD to move and click to attack enemies.", "Tip: Collect resources to craft better equipment.", "Tip: Your combat skill increases as you defeat enemies.", "Tip: Look for points of interest marked on the minimap.", "Tip: Different biomes have different resources and enemies.", "Tip: Pets can help you in combat and provide bonuses." ], whatNext: [ "Try exploring new areas to find resources and level up.", "You could hunt some enemies to gain XP and loot.", "Check your inventory - maybe craft some new equipment.", "Have you discovered all the points of interest on this planet?" ], getStronger: [ "Fight enemies to gain combat XP and level up your skills.", "Craft better weapons and armor from the resources you gather.", "Find and bond with a pet companion for stat bonuses.", "Complete daily challenges for bonus rewards.", "Unlock talents in the talent tree as you level up." ], enemies: [ "Enemies respawn periodically throughout the world.", "Look for the red markers on your minimap.", "Elite enemies (marked with special effects) drop better loot.", "Different biomes have different enemy types." ] }; // v6.37: Epic Space Opera Narrator responses (local fallback) const EPIC_NARRATOR_RESPONSES = { greeting: [ "And so it begins anew... The Leviathan stirs from its slumber, its sensors awakening to the infinite void. What cosmic destiny awaits?", "From the darkness between stars, the legend emerges once more. The Omniverse holds its breath as the Leviathan prepares to write another chapter in its eternal saga.", "Across lightyears of silent void, a mechanical champion awakens. The cosmos itself seems to whisper: the Leviathan has returned." ], lowHealth: [ "The Leviathan falters! Its hull integrity crumbles like dying stars. Yet even now, at the precipice of oblivion, the legend refuses to fade!", "Warning klaxons echo through the cosmos as the probe's systems scream in mechanical anguish. But heroes are forged in the fires of near-destruction!", "Critical damage tears through the Leviathan's frame. The void reaches out with cold fingers... but not today. NOT TODAY!" ], nearEnemy: [ "The sensors detect movement in the darkness. Something stirs... something ancient and hostile. The dance of destruction is about to begin!", "From the shadows of this alien world, adversaries emerge! The Leviathan's combat systems hum with anticipation. Battle is inevitable!", "A disturbance ripples through the cosmic fabric. Enemies approach! Let them come - they shall learn why legends are feared!" ], afterKill: [ "ANOTHER FOE VANQUISHED! The Leviathan's combat record grows ever more legendary. The cosmos trembles at such prowess!", "Victory! The enemy crumbles before the mechanical might of the Leviathan. Let this triumph echo across the stars!", "And so falls another who dared challenge the legend. The probe stands victorious, its legacy written in the scattered remains of the fallen!" ], exploration: [ "The Leviathan ventures forth into the unknown, where no probe has dared tread. What secrets slumber in these uncharted reaches?", "Across this alien landscape, the legend continues its eternal journey. Every step writes history, every discovery reshapes destiny.", "The horizon beckons with promises of wonder and peril alike. The Leviathan answers, for exploration is its very essence!" ], tips: [ "Ancient wisdom echoes through the void: mastery of movement separates the legendary from the forgotten. WASD - the keys to cosmic navigation!", "The archives speak of resources scattered across worlds like cosmic breadcrumbs. Gather them, and forge equipment worthy of legend!", "In the eternal struggle between machine and monster, experience is the true currency. Each battle makes the Leviathan stronger!", "The minimap reveals points of interest like stars in the darkness. Seek them out, and uncover the universe's hidden truths!", "Every biome holds unique challenges and treasures. The wise explorer adapts, the legendary explorer conquers all!" ], whatNext: [ "The universe sprawls endlessly before the Leviathan. Perhaps undiscovered territories await, pregnant with possibility and peril!", "Enemies still roam this world, their defeat necessary for the legend to grow. Hunt them, and let experience flow like starlight!", "The inventory holds potential waiting to be realized. What magnificent equipment might be forged from gathered resources?", "Mysteries yet remain on this world - points of interest unexplored, secrets undiscovered. The saga is far from complete!" ], getStronger: [ "Power is earned through conflict! Each enemy vanquished adds to the Leviathan's growing legend. Seek battle, embrace victory!", "From the bones of worlds and the hearts of stars, craft equipment that shall make the cosmos itself take notice!", "A companion drone awaits bonding - together, probe and pet shall become an unstoppable force of cosmic destiny!", "Daily challenges offer paths to power beyond normal reach. Complete them, and ascend to new heights of legend!", "The talent tree branches like the arms of galaxies. Choose wisely, and watch the Leviathan transcend its former limits!" ], enemies: [ "The hostile denizens of this world regenerate like hydra heads - cut one down, and another rises. Endless combat, endless glory!", "Red markers on the minimap signal where adversaries lurk. They are not warnings - they are invitations to legend!", "Elite enemies burn with power beyond their lesser kin. Seek them out! Greater challenges yield greater rewards!", "Each biome spawns its own unique horrors. The Leviathan fears none of them - for it IS the terror of the cosmos!" ], default: [ "The cosmic narrator pauses, contemplating the infinite mysteries of the Omniverse. Ask of health, enemies, or destiny itself!", "Across the tapestry of stars, the Leviathan awaits your command. Speak, and let the saga continue!", "The void listens. The stars observe. What epic query burns within your heart, commander of legends?", "Every word exchanged adds to the chronicle. The narrator stands ready to illuminate the path ahead!" ] }; // ============================================ // v6.35: CHRONICLE ENGINE // AI-powered narrative history generation // ============================================ const CHRONICLE_STYLES = { epic: { name: 'Epic Space Opera', prompt: `You are the COSMIC CHRONICLER, weaving the eternal saga of the Leviathan. Write in THIRD PERSON with sweeping, cinematic grandeur. Use powerful verbs: "vanquished", "descended", "emerged", "conquered", "transcended". Reference cosmic forces, destiny, and legend. Channel the gravitas of Dune, Star Wars, and classic space opera. Make each entry feel like it belongs in an ancient tome of galactic history.`, titlePrefixes: ['The Day', 'When Darkness', 'Victory at', 'The Fall of', 'Rise of the', 'Chronicle of'] }, documentary: { name: 'Documentary', prompt: `You are a SCIENTIFIC OBSERVER recording the Leviathan's mission. Write in analytical, third-person documentary style. Include timestamps and precise observations. Note statistical achievements and tactical decisions. Maintain objectivity while conveying the significance of events. Think nature documentary meets space exploration log.`, titlePrefixes: ['Mission Log:', 'Observation:', 'Event Record:', 'Analysis:', 'Report:', 'Survey:'] }, poetic: { name: 'Poetic & Mystical', prompt: `You are a MYSTICAL BARD singing of the Leviathan's journey. Write in flowing, lyrical verse with metaphor and symbolism. Reference the dance of stars, the whispers of void, cosmic harmonies. Each entry should read like space poetry or a creation myth. Evoke wonder, beauty, and the sublime nature of existence.`, titlePrefixes: ['Song of', 'Whispers from', 'The Dream of', 'Starlight Upon', 'Ode to', 'Verse of'] }, hardboiled: { name: 'Hard-Boiled Noir', prompt: `You are a GRIZZLED NARRATOR telling it like it is. Write in terse, punchy noir style. Short sentences. Hard truths. The cosmos is a cold dame who doesn't play fair. Mix cynicism with unexpected moments of grim humor. Every victory has a cost. Every defeat leaves scars. Think Raymond Chandler meets Cowboy Bebop.`, titlePrefixes: ['Another Day in', 'The Job at', 'Dead End on', 'No Good Deed:', 'Cold Case:', 'Last Stand at'] } }; const CHRONICLE_EVENT_TYPES = { boss_defeat: { weight: 5, icon: '⚔️', color: '#ffd700' }, elite_defeat: { weight: 3, icon: '🗡️', color: '#ff8c00' }, player_fainted: { weight: 4, icon: '💀', color: '#ff4444' }, skill_levelup: { weight: 2, icon: '⬆️', color: '#00ff00' }, planet_discovered: { weight: 3, icon: '🌍', color: '#4488ff' }, lore_found: { weight: 2, icon: '📜', color: '#aa88ff' }, pet_acquired: { weight: 3, icon: '🐾', color: '#ff88aa' }, milestone: { weight: 4, icon: '🏆', color: '#ffdd00' }, portal_cleared: { weight: 4, icon: '🌀', color: '#aa00ff' }, rare_item: { weight: 3, icon: '💎', color: '#00ffff' } }; // Capture chronicle event function captureChronicleEvent(eventType, metadata = {}) { if (!gameData.chronicle) { gameData.chronicle = { entries: [], eventBuffer: [], settings: { autoGenerate: true, narrativeStyle: 'epic', eventThreshold: 3 }, stats: { totalEntries: 0, lastGenerated: null } }; } const event = { id: Date.now() + Math.random().toString(36).substr(2, 9), type: eventType, timestamp: Date.now(), playtime: gameData.playtime || 0, planet: activeCiv?.name || 'Unknown Space', planetType: activeCiv?.type || 'void', metadata: { ...metadata, playerHp: gameData.player?.hp, playerMaxHp: gameData.player?.maxHp, statistics: { ...gameData.statistics } } }; gameData.eventBuffer = gameData.eventBuffer || []; gameData.eventBuffer.push(event); gameData.chronicle.eventBuffer.push(event); // Update UI updateChronicleUI(); // Auto-generate if threshold reached const threshold = gameData.chronicle.settings.eventThreshold || 3; if (gameData.chronicle.settings.autoGenerate && gameData.chronicle.eventBuffer.length >= threshold) { // Slight delay to not interrupt gameplay setTimeout(() => generateChronicleEntry(), 2000); } saveGameData(); } // Generate chronicle entry from buffered events async function generateChronicleEntry() { if (!gameData.chronicle?.eventBuffer?.length) { showNotification('No events to chronicle yet. Keep exploring!', 'info'); return; } const style = gameData.chronicle.settings.narrativeStyle || 'epic'; const styleConfig = CHRONICLE_STYLES[style]; const events = [...gameData.chronicle.eventBuffer]; // Clear buffer gameData.chronicle.eventBuffer = []; gameData.eventBuffer = []; // Build event summary for AI const eventSummary = events.map(e => { const typeInfo = CHRONICLE_EVENT_TYPES[e.type] || { icon: '📌' }; return `${typeInfo.icon} ${e.type.replace(/_/g, ' ').toUpperCase()}: ${JSON.stringify(e.metadata)} on ${e.planet}`; }).join('\n'); const prompt = `${styleConfig.prompt} Based on these events that just occurred during the Leviathan's journey, write a single chronicle entry (2-3 paragraphs): EVENTS: ${eventSummary} CURRENT STATUS: - Location: ${activeCiv?.name || 'Deep Space'} - Playtime: ${Math.floor((gameData.playtime || 0) / 60)} minutes - Bosses Defeated: ${gameData.statistics?.bossesDefeated || 0} - Mobs Defeated: ${gameData.statistics?.mobsKilled || 0} Write a dramatic, engaging chronicle entry that weaves these events into a compelling narrative. Include a short, punchy TITLE on the first line (without "Title:" prefix).`; // Check if RAPPID is available if (rappidSettings.rappid && getActiveEndpoint()) { try { showNotification('📜 Generating chronicle entry...', 'info'); const response = await generateCopilotResponseWithRappid(prompt, []); if (response) { const lines = response.trim().split('\n'); const title = lines[0].replace(/^#+ /, '').replace(/\*\*/g, '').trim(); const content = lines.slice(1).join('\n').trim(); addChronicleEntry(title, content, events); showNotification('📜 Chronicle entry created!', 'success'); return; } } catch (error) { console.error('Chronicle AI generation failed:', error); } } // Fallback: Generate local chronicle generateLocalChronicleEntry(events, styleConfig); } // Local fallback chronicle generation function generateLocalChronicleEntry(events, styleConfig) { const mainEvent = events.reduce((max, e) => { const weight = CHRONICLE_EVENT_TYPES[e.type]?.weight || 1; const maxWeight = CHRONICLE_EVENT_TYPES[max.type]?.weight || 1; return weight > maxWeight ? e : max; }, events[0]); const titlePrefix = styleConfig.titlePrefixes[Math.floor(Math.random() * styleConfig.titlePrefixes.length)]; const location = mainEvent.planet || 'the Void'; const titles = { boss_defeat: `${titlePrefix} ${location}'s Guardian`, elite_defeat: `${titlePrefix} the Elite Hunt`, player_fainted: `${titlePrefix} Darkness`, skill_levelup: `${titlePrefix} Growing Power`, planet_discovered: `${titlePrefix} New Horizons`, lore_found: `${titlePrefix} Ancient Secrets`, pet_acquired: `${titlePrefix} a New Bond`, milestone: `${titlePrefix} Achievement`, portal_cleared: `${titlePrefix} the Rift`, rare_item: `${titlePrefix} Cosmic Treasure` }; const narratives = { epic: { boss_defeat: `The battle that shook the very foundations of ${location} shall be remembered for eons. The Leviathan, battered but unbowed, faced the guardian of this realm in combat most fierce. When the final blow landed, silence fell across the cosmos - for another legend had been written in starfire and determination.`, elite_defeat: `Through the chaos of battle, the Leviathan carved a path of triumph. Elite adversaries fell before its might, their enhanced forms no match for the determination burning in the probe's core systems.`, player_fainted: `Even legends know darkness. The Leviathan fell, systems failing, consciousness fading into the void. But the cosmos is not done with this champion - not yet. From the ashes of defeat, the saga continues.`, skill_levelup: `Power surged through the Leviathan's systems as new capabilities awakened. What was once impossible now lies within reach. The universe takes notice when a legend grows stronger.`, default: `The journey continues across the infinite tapestry of stars. Each moment adds to the legend, each choice shapes destiny itself.` }, documentary: { boss_defeat: `[PRIORITY LOG] Major combat engagement concluded at ${location}. Target designation: Boss-class entity. Result: Successful termination. Systems sustained moderate damage but remain operational. This engagement marks a significant milestone in the mission parameters.`, elite_defeat: `[COMBAT LOG] Elite-class hostile neutralized. Tactical analysis indicates improved combat efficiency compared to previous engagements. Resources expended within acceptable parameters.`, player_fainted: `[CRITICAL EVENT] System failure recorded. All primary functions experienced temporary shutdown. Auto-recovery protocols engaged successfully. Inventory contents dispersed at failure coordinates.`, skill_levelup: `[DEVELOPMENT LOG] Capability enhancement detected. New operational parameters unlocked. Performance metrics indicate ${Math.floor(Math.random() * 15 + 5)}% improvement in relevant subsystems.`, default: `[MISSION LOG] Standard operations continue. Environmental survey ongoing. No anomalies detected beyond expected parameters.` }, poetic: { boss_defeat: `In the dance of light and shadow, the great one fell - not with rage, but with the quiet acceptance of cosmic order. The Leviathan sang its victory to the watching stars, and somewhere in the void, the universe wept beautiful tears of stardust.`, elite_defeat: `Swift as thought, fierce as dying suns, the battle bloomed like a deadly flower. When petals fell, only the Leviathan remained, baptized in the light of conquest.`, player_fainted: `Into the gentle dark the wanderer slipped, cradled by the void's cold embrace. But even in that endless night, a spark remained - a promise of dawn yet to come.`, skill_levelup: `Like a chrysalis cracking, like a star being born, transformation whispered through ancient circuits. What emerges now is something more - something the cosmos has been waiting for.`, default: `The journey is the poem, and we are all merely verses in its infinite stanzas. Onward, ever onward, toward horizons that dream of our arrival.` }, hardboiled: { boss_defeat: `The big guy went down hard. Harder than I expected, actually. ${location} won't forget this fight anytime soon - neither will I. Sometimes the cosmos gives you a break. Today was one of those days.`, elite_defeat: `Another tough customer who thought they could take me. They thought wrong. The void's got no shortage of these wannabe killers, but there's only one Leviathan.`, player_fainted: `I hit the deck. Everything went black. When I came to, my stuff was scattered across the ground like confetti at a funeral. Not my finest moment, but I've had worse. Probably.`, skill_levelup: `Something clicked. New tricks, new moves. In this business, you either get better or you get dead. Today, I got better.`, default: `Another day in the infinite grind. The cosmos doesn't care about your problems, and neither do I. Keep moving.` } }; const style = gameData.chronicle.settings.narrativeStyle || 'epic'; const styleNarratives = narratives[style] || narratives.epic; const title = titles[mainEvent.type] || `${titlePrefix} Unknown Events`; const content = styleNarratives[mainEvent.type] || styleNarratives.default; // Add context about other events let fullContent = content; if (events.length > 1) { const otherEvents = events.filter(e => e !== mainEvent).slice(0, 2); const additions = otherEvents.map(e => { const typeInfo = CHRONICLE_EVENT_TYPES[e.type]; return `${typeInfo?.icon || '•'} ${e.type.replace(/_/g, ' ')}`; }).join(', '); fullContent += `\n\nAlso recorded: ${additions}`; } addChronicleEntry(title, fullContent, events); showNotification('📜 Chronicle entry recorded!', 'success'); } // Add entry to chronicle function addChronicleEntry(title, content, events) { const entry = { id: Date.now() + Math.random().toString(36).substr(2, 9), timestamp: Date.now(), title: title, content: content, events: events.map(e => ({ type: e.type, planet: e.planet })), style: gameData.chronicle.settings.narrativeStyle }; gameData.chronicle.entries.unshift(entry); // Newest first gameData.chronicle.stats.totalEntries++; gameData.chronicle.stats.lastGenerated = Date.now(); // Keep max 50 entries if (gameData.chronicle.entries.length > 50) { gameData.chronicle.entries = gameData.chronicle.entries.slice(0, 50); } saveGameData(); updateChronicleUI(); } // Update chronicle UI display function updateChronicleUI() { const countEl = document.getElementById('chronicle-count'); const pendingEl = document.getElementById('chronicle-pending'); const entriesEl = document.getElementById('chronicle-entries'); const styleEl = document.getElementById('chronicle-style'); if (countEl) countEl.textContent = gameData.chronicle?.entries?.length || 0; if (pendingEl) pendingEl.textContent = gameData.chronicle?.eventBuffer?.length || 0; if (styleEl && gameData.chronicle?.settings?.narrativeStyle) { styleEl.value = gameData.chronicle.settings.narrativeStyle; } if (entriesEl && gameData.chronicle?.entries?.length > 0) { entriesEl.innerHTML = gameData.chronicle.entries.map(entry => { const date = new Date(entry.timestamp); const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); const eventIcons = entry.events?.map(e => CHRONICLE_EVENT_TYPES[e.type]?.icon || '📌').join(' ') || ''; return `
${entry.title}
${dateStr}
${entry.content}
${eventIcons}
`; }).join(''); } } // Update chronicle style setting function updateChronicleStyle() { const styleEl = document.getElementById('chronicle-style'); if (styleEl && gameData.chronicle) { gameData.chronicle.settings.narrativeStyle = styleEl.value; saveGameData(); showNotification(`Chronicle style set to: ${CHRONICLE_STYLES[styleEl.value]?.name || styleEl.value}`, 'info'); } } // Export chronicle as markdown/JSON function exportChronicle() { if (!gameData.chronicle?.entries?.length) { showNotification('No chronicle entries to export yet!', 'info'); return; } const markdown = `# The Chronicle of Leviathan ## A Captain's Log of the Omniverse *Generated: ${new Date().toISOString()}* *Total Entries: ${gameData.chronicle.entries.length}* *Playtime: ${Math.floor((gameData.playtime || 0) / 60)} minutes* --- ${gameData.chronicle.entries.map(entry => { const date = new Date(entry.timestamp); return `### ${entry.title} *${date.toLocaleDateString()} ${date.toLocaleTimeString()}* ${entry.content} --- `; }).join('\n')} *Chronicle generated by LEVIATHAN: OMNIVERSE* *AI-powered narrative engine v6.35* `; const blob = new Blob([markdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `leviathan-chronicle-${new Date().toISOString().split('T')[0]}.md`; link.click(); URL.revokeObjectURL(url); showNotification('📤 Chronicle exported as Markdown!', 'success'); } function initCopilotCompanion() { // Initialize speech recognition if available (browser fallback) if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; copilotVoiceRecognition = new SpeechRecognition(); copilotVoiceRecognition.continuous = false; copilotVoiceRecognition.interimResults = true; // v5.9: Enable interim results copilotVoiceRecognition.lang = 'en-US'; copilotVoiceRecognition.onresult = (event) => { let interimTranscript = ''; let finalTranscript = ''; for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interimTranscript += transcript; } } // Update overlay with real-time transcription if (finalTranscript) { updateSTTTranscript(finalTranscript, true); } else if (interimTranscript) { updateSTTTranscript(interimTranscript, false); } }; copilotVoiceRecognition.onend = () => { copilotIsListening = false; document.getElementById('copilot-voice-btn').classList.remove('recording'); }; copilotVoiceRecognition.onerror = (event) => { copilotIsListening = false; document.getElementById('copilot-voice-btn').classList.remove('recording'); showSTTOverlay(false); console.error('Browser STT error:', event.error); }; } } function createCopilotMesh() { // v9.10: Skip copilot orb in customOnly worlds if (window.WORLD_SYSTEMS?.customOnly === true) return; if (copilotMesh) { scene.remove(copilotMesh); copilotMesh = null; } if (mode !== 'world' || !worldState.player) return; const companionGroup = new THREE.Group(); // Main orb const orbGeometry = new THREE.SphereGeometry(0.5, 24, 24); const orbMaterial = new THREE.MeshStandardMaterial({ color: COPILOT_CONFIG.color, emissive: COPILOT_CONFIG.color, emissiveIntensity: 0.6, metalness: 0.8, roughness: 0.2, transparent: true, opacity: 0.9 }); const orb = new THREE.Mesh(orbGeometry, orbMaterial); companionGroup.add(orb); // Inner glow core - fog: false so it's always visible const coreGeometry = new THREE.SphereGeometry(0.3, 16, 16); const coreMaterial = new THREE.MeshBasicMaterial({ color: COPILOT_CONFIG.glowColor, transparent: true, opacity: 0.8, fog: false // v6.1: Pierces through fog }); const core = new THREE.Mesh(coreGeometry, coreMaterial); companionGroup.add(core); // Outer glow const glowGeometry = new THREE.SphereGeometry(0.7, 16, 16); const glowMaterial = new THREE.MeshBasicMaterial({ color: COPILOT_CONFIG.glowColor, transparent: true, opacity: 0.2, side: THREE.BackSide, fog: false // v6.1: Pierces through fog }); const glow = new THREE.Mesh(glowGeometry, glowMaterial); companionGroup.add(glow); // v6.1: FOG-PIERCING BEACON - Large outer glow visible even in thick fog const beaconGeometry = new THREE.SphereGeometry(2.5, 16, 16); const beaconMaterial = new THREE.MeshBasicMaterial({ color: COPILOT_CONFIG.glowColor, transparent: true, opacity: 0.08, side: THREE.BackSide, fog: false, // Always visible through fog blending: THREE.AdditiveBlending, depthWrite: false }); const beacon = new THREE.Mesh(beaconGeometry, beaconMaterial); companionGroup.add(beacon); // v6.1: Secondary pulsing beacon ring for visibility const beaconRingGeometry = new THREE.TorusGeometry(1.8, 0.15, 8, 32); const beaconRingMaterial = new THREE.MeshBasicMaterial({ color: COPILOT_CONFIG.glowColor, transparent: true, opacity: 0.15, fog: false, blending: THREE.AdditiveBlending }); const beaconRing = new THREE.Mesh(beaconRingGeometry, beaconRingMaterial); beaconRing.rotation.x = Math.PI / 2; companionGroup.add(beaconRing); // Point light for illumination - increased range for fog const light = new THREE.PointLight(COPILOT_CONFIG.glowColor, 2.5, 20); companionGroup.add(light); // Particle ring const particleGeometry = new THREE.BufferGeometry(); const particleCount = COPILOT_CONFIG.particleCount; const positions = new Float32Array(particleCount * 3); for (let i = 0; i < particleCount * 3; i += 3) { const angle = (i / 3) * (Math.PI * 2 / particleCount); const radius = 0.8 + Math.random() * 0.3; positions[i] = Math.cos(angle) * radius; positions[i + 1] = (Math.random() - 0.5) * 0.4; positions[i + 2] = Math.sin(angle) * radius; } particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const particleMaterial = new THREE.PointsMaterial({ color: COPILOT_CONFIG.glowColor, size: 0.1, // v6.1: Slightly larger for fog visibility transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, fog: false // v6.1: Particles pierce through fog }); const particles = new THREE.Points(particleGeometry, particleMaterial); companionGroup.add(particles); // v9.9: Click collider for easier selection (very low opacity to allow raycast) const clickColliderGeometry = new THREE.SphereGeometry(1.5, 8, 8); const clickColliderMaterial = new THREE.MeshBasicMaterial({ color: COPILOT_CONFIG.glowColor, transparent: true, opacity: 0.01, // Nearly invisible but still raycastable depthWrite: false }); const clickCollider = new THREE.Mesh(clickColliderGeometry, clickColliderMaterial); clickCollider.name = 'companionClickCollider'; companionGroup.add(clickCollider); // Store references for animation companionGroup.userData = { orb: orb, core: core, glow: glow, light: light, particles: particles, beacon: beacon, // v6.1: Fog-piercing beacon beaconRing: beaconRing, // v6.1: Pulsing ring clickCollider: clickCollider, // v9.9: Click detection collider isClickable: true, isCopilot: true }; // v5.11: Create text groups for Star Wars crawl - optimized for cinematic camera // v6.33: Adjusted positions to be closer to robot and more visible copilotTextGroup = new THREE.Group(); copilotTextGroup.position.set(0, 3, 2); // Lower and closer to robot copilotTextGroup.rotation.x = THREE.MathUtils.degToRad(-35); // Less steep tilt companionGroup.add(copilotTextGroup); copilotPersistentTextGroup = new THREE.Group(); copilotPersistentTextGroup.position.set(0, 2.5, 1); // Much closer to robot, slightly above copilotPersistentTextGroup.rotation.x = THREE.MathUtils.degToRad(-25); // Gentle tilt toward camera companionGroup.add(copilotPersistentTextGroup); // Load font for 3D text loadCopilotTextFont(); // Position initially near player if (worldState.player) { companionGroup.position.copy(worldState.player.position); companionGroup.position.y += COPILOT_CONFIG.floatHeight; companionGroup.position.z += COPILOT_CONFIG.followDistance; } scene.add(companionGroup); copilotMesh = companionGroup; } // v5.10: Load font for 3D text rendering function loadCopilotTextFont() { if (copilotTextFont) return; // Already loaded const loader = new THREE.FontLoader(); loader.load('https://threejs.org/examples/fonts/helvetiker_regular.typeface.json', (font) => { copilotTextFont = font; titleTextFont = font; // v6.19: Share font with title system console.log('Copilot 3D text font loaded'); }, undefined, (err) => { console.warn('Failed to load 3D text font:', err); }); } // v6.26: CINEMATIC 3D TITLE with particle effects, glow, and animations let titleAnimationFrameId = null; let titleTargetPosition = new THREE.Vector3(); let titleParticles = null; // Sparkle particles let titleGlowPlane = null; // Backdrop glow let titleShinePhase = 0; // For shine sweep effect // v6.27: Orbital path visualization let orbitalPathLine = null; function create3DTitleText() { if (!titleTextFont) { setTimeout(create3DTitleText, 500); return; } // Clean up any existing title remove3DTitle(); titleTextGroup = new THREE.Group(); // v6.29: LARGER SCALE for prominent title at galaxy center // Clean presentation like original HTML text - no extra effects const GALAXY_SCALE = 90; // === MAIN TITLE: LEVIATHAN === const titleMaterial = new THREE.MeshStandardMaterial({ color: 0x00ffff, emissive: 0x00ddff, emissiveIntensity: 0.6, metalness: 0.3, roughness: 0.4, toneMapped: false }); try { const titleGeometry = new THREE.TextGeometry('LEVIATHAN', { font: titleTextFont, size: 1.2 * GALAXY_SCALE, height: 0.2 * GALAXY_SCALE, curveSegments: 12, bevelEnabled: true, bevelThickness: 0.04 * GALAXY_SCALE, bevelSize: 0.025 * GALAXY_SCALE, bevelSegments: 4 }); titleGeometry.computeBoundingBox(); titleGeometry.center(); const titleMesh = new THREE.Mesh(titleGeometry, titleMaterial); titleMesh.position.set(0, 0.6 * GALAXY_SCALE, 0); titleMesh.userData.baseColor = new THREE.Color(0x00ffff); titleMesh.userData.baseEmissive = new THREE.Color(0x00ddff); titleMesh.userData.galaxyScale = GALAXY_SCALE; titleTextGroup.add(titleMesh); // === SUBTITLE: GALAXY SIMULATION === const subtitleMaterial = new THREE.MeshStandardMaterial({ color: 0x8899aa, emissive: 0x556677, emissiveIntensity: 0.3, metalness: 0.2, roughness: 0.5, toneMapped: false }); const subtitleGeometry = new THREE.TextGeometry('GALAXY SIMULATION v10.19', { font: titleTextFont, size: 0.35 * GALAXY_SCALE, height: 0.05 * GALAXY_SCALE, curveSegments: 6, bevelEnabled: true, bevelThickness: 0.01 * GALAXY_SCALE, bevelSize: 0.008 * GALAXY_SCALE, bevelSegments: 2 }); subtitleGeometry.computeBoundingBox(); subtitleGeometry.center(); const subtitleMesh = new THREE.Mesh(subtitleGeometry, subtitleMaterial); subtitleMesh.position.set(0, -0.5 * GALAXY_SCALE, 0); subtitleMesh.userData.baseEmissive = new THREE.Color(0x556677); subtitleMesh.userData.galaxyScale = GALAXY_SCALE; titleTextGroup.add(subtitleMesh); // Add to scene (v6.28: clean presentation, no extra effects) scene.add(titleTextGroup); // Initialize position updateTitleTargetPosition(); titleTextGroup.position.copy(titleTargetPosition); // Hide HTML title const htmlTitle = document.querySelector('.game-title'); const htmlSubtitle = document.querySelector('.subtitle'); if (htmlTitle) htmlTitle.style.opacity = '0'; if (htmlSubtitle) htmlSubtitle.style.opacity = '0'; // Start animation loop startTitleAnimation(); console.log('Clean 3D title created (v6.28)'); } catch (e) { console.warn('Failed to create 3D title text:', e); } } // Create radial glow texture for backdrop function createGlowTexture() { const canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256; const ctx = canvas.getContext('2d'); const gradient = ctx.createRadialGradient(128, 128, 0, 128, 128, 128); gradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); gradient.addColorStop(0.3, 'rgba(255, 255, 255, 0.5)'); gradient.addColorStop(0.6, 'rgba(255, 255, 255, 0.1)'); gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); ctx.fillStyle = gradient; ctx.fillRect(0, 0, 256, 256); const texture = new THREE.CanvasTexture(canvas); return texture; } // v6.27: Create accretion disk around black hole function createAccretionDisk(scale) { const diskGeometry = new THREE.RingGeometry(2 * scale, 8 * scale, 64); const diskMaterial = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.3, side: THREE.DoubleSide, blending: THREE.AdditiveBlending, toneMapped: false }); const disk = new THREE.Mesh(diskGeometry, diskMaterial); disk.rotation.x = Math.PI / 2.2; // Slightly tilted disk.userData.isAccretionDisk = true; titleTextGroup.add(disk); // Inner hot ring const innerRingGeometry = new THREE.RingGeometry(1.5 * scale, 2.5 * scale, 64); const innerRingMaterial = new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 0.5, side: THREE.DoubleSide, blending: THREE.AdditiveBlending, toneMapped: false }); const innerRing = new THREE.Mesh(innerRingGeometry, innerRingMaterial); innerRing.rotation.x = Math.PI / 2.2; innerRing.userData.isAccretionDisk = true; titleTextGroup.add(innerRing); } // Create sparkle particle system (scaled for galaxy view) function createTitleParticles(scale = 1) { const particleCount = 80; // More particles for galaxy scale const positions = new Float32Array(particleCount * 3); const velocities = []; const sizes = new Float32Array(particleCount); for (let i = 0; i < particleCount; i++) { // Spread particles around black hole area positions[i * 3] = (Math.random() - 0.5) * 10 * scale; positions[i * 3 + 1] = (Math.random() - 0.5) * 3 * scale; positions[i * 3 + 2] = (Math.random() - 0.5) * 2 * scale; velocities.push({ x: (Math.random() - 0.5) * 0.5 * scale, y: (Math.random() - 0.5) * 0.5 * scale, z: (Math.random() - 0.5) * 0.3 * scale, phase: Math.random() * Math.PI * 2 }); sizes[i] = (Math.random() * 0.15 + 0.05) * scale; } const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); geometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); const starTexture = createStarTexture(); const material = new THREE.PointsMaterial({ size: 3 * scale, map: starTexture, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false, vertexColors: false, color: 0x88ddff }); const particles = new THREE.Points(geometry, material); particles.userData.velocities = velocities; particles.userData.scale = scale; return particles; } // Create star/sparkle texture function createStarTexture() { const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 64; const ctx = canvas.getContext('2d'); // Draw 4-point star ctx.fillStyle = 'white'; ctx.beginPath(); const cx = 32, cy = 32; for (let i = 0; i < 4; i++) { const angle = (i * Math.PI / 2) - Math.PI / 4; const innerAngle = angle + Math.PI / 4; ctx.lineTo(cx + Math.cos(angle) * 30, cy + Math.sin(angle) * 30); ctx.lineTo(cx + Math.cos(innerAngle) * 8, cy + Math.sin(innerAngle) * 8); } ctx.closePath(); ctx.fill(); // Add glow const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32); gradient.addColorStop(0, 'rgba(255, 255, 255, 0.8)'); gradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.2)'); gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); ctx.globalCompositeOperation = 'source-over'; ctx.fillStyle = gradient; ctx.fillRect(0, 0, 64, 64); return new THREE.CanvasTexture(canvas); } // Create lens flare accent lights (scaled for galaxy view) function createLensFlares(galaxyScale = 1) { const flarePositions = [ { x: -4.5, y: 0.8, scale: 0.3, color: 0x00ffff }, // Left edge { x: 4.5, y: 0.8, scale: 0.25, color: 0x00ddff }, // Right edge { x: 0, y: 1.2, scale: 0.2, color: 0xffffff } // Top center ]; const flareTexture = createGlowTexture(); flarePositions.forEach(pos => { const flareMaterial = new THREE.SpriteMaterial({ map: flareTexture, color: pos.color, transparent: true, opacity: 0.6, blending: THREE.AdditiveBlending, toneMapped: false }); const flare = new THREE.Sprite(flareMaterial); const scaledSize = pos.scale * galaxyScale; flare.scale.set(scaledSize, scaledSize, 1); flare.position.set(pos.x * galaxyScale, pos.y * galaxyScale, 0.1 * galaxyScale); flare.userData.isFlare = true; flare.userData.baseScale = scaledSize; titleTextGroup.add(flare); }); } // v6.27: Title is now the SUPERMASSIVE BLACK HOLE at galaxy center // It stays fixed at (0, 0, 0) - the gravitational center of the galaxy const BLACKHOLE_POSITION = new THREE.Vector3(0, 0, 0); const TITLE_SCALE = 80; // Scale up for visibility from galaxy view distance function updateTitleTargetPosition() { // Title stays FIXED at galaxy center - it IS the black hole titleTargetPosition.copy(BLACKHOLE_POSITION); } // v6.28: Clean title animation - fixed at galaxy center, no flashy effects function startTitleAnimation() { if (titleAnimationFrameId) { cancelAnimationFrame(titleAnimationFrameId); } function animateTitle() { if (!titleTextGroup || mode !== 'galaxy') { titleAnimationFrameId = null; return; } // v8.34: Skip animation when tab is hidden if (!isPageVisible) { titleAnimationFrameId = requestAnimationFrame(animateTitle); return; } // v6.28: Title stays FIXED at galaxy center (0,0,0) titleTextGroup.position.copy(BLACKHOLE_POSITION); // Face the camera from the center titleTextGroup.lookAt(camera.position); titleAnimationFrameId = requestAnimationFrame(animateTitle); } animateTitle(); } // v6.22: Animate the 3D title (API compatibility) function animate3DTitle(deltaTime) { // Animation is self-contained in startTitleAnimation() } // v6.26: Remove 3D title and all effects when leaving galaxy function remove3DTitle() { if (titleAnimationFrameId) { cancelAnimationFrame(titleAnimationFrameId); titleAnimationFrameId = null; } if (titleTextGroup) { scene.remove(titleTextGroup); titleTextGroup.children.forEach(child => { if (child.geometry) child.geometry.dispose(); if (child.material) { if (child.material.map) child.material.map.dispose(); child.material.dispose(); } }); titleTextGroup = null; } // Clear references titleParticles = null; titleGlowPlane = null; titleShinePhase = 0; // Show HTML title again const htmlTitle = document.querySelector('.game-title'); const htmlSubtitle = document.querySelector('.subtitle'); if (htmlTitle) htmlTitle.style.opacity = '1'; if (htmlSubtitle) htmlSubtitle.style.opacity = '1'; } // v5.10: Animate voice response as Star Wars text crawl function animateCopilotTextCrawl(text) { if (!copilotTextFont || !copilotTextGroup || !copilotMesh) { console.log('Text crawl not ready - font or groups not initialized'); return; } // Cancel any existing animation if (copilotActiveTextAnimation) { cancelAnimationFrame(copilotActiveTextAnimation); copilotActiveTextAnimation = null; } // Clear existing text meshes copilotTextMeshes.forEach(mesh => { if (mesh.geometry) mesh.geometry.dispose(); if (mesh.material) mesh.material.dispose(); copilotTextGroup.remove(mesh); }); copilotTextMeshes = []; copilotTextGroup.position.y = 3; // v6.33: Reset to closer position // v5.11: Word wrap text into lines // v6.33: Longer lines for full response display const maxCharsPerLine = 30; const words = text.split(' '); const lines = []; let currentLine = ''; words.forEach(word => { if ((currentLine + word).length > maxCharsPerLine) { if (currentLine) { lines.push(currentLine.trim()); currentLine = ''; } } currentLine += word + ' '; }); if (currentLine) { lines.push(currentLine.trim()); } // v5.11: Create 3D text for each line // v6.33: Adjusted for full response display const lineHeight = 0.55; const textMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0x06ffa5, emissiveIntensity: 0.8, // Brighter glow metalness: 0.4, roughness: 0.3, transparent: true, opacity: 1 }); lines.forEach((line, index) => { try { const textGeometry = new THREE.TextGeometry(line, { font: copilotTextFont, size: 0.38, // v6.33: Slightly smaller for longer lines height: 0.03, curveSegments: 4, bevelEnabled: true, bevelThickness: 0.008, bevelSize: 0.005, bevelSegments: 2 }); textGeometry.center(); const textMesh = new THREE.Mesh(textGeometry, textMaterial.clone()); textMesh.position.y = -index * lineHeight; copilotTextGroup.add(textMesh); copilotTextMeshes.push(textMesh); } catch (e) { console.warn('Failed to create text geometry for line:', line, e); } }); const totalHeight = lines.length * lineHeight; animateStarWarsScroll(totalHeight, text); } // v5.11: Animate the scrolling like Star Wars // v6.33: Adjusted for full response display function animateStarWarsScroll(totalHeight, fullText) { const scrollSpeed = 0.03; // Smooth scroll speed const startY = 0; const endY = totalHeight + 5; // Scroll until all text passes let currentY = startY; const animate = () => { if (!copilotTextGroup || copilotTextMeshes.length === 0) return; currentY += scrollSpeed; copilotTextGroup.position.y = 3 + currentY; // v6.33: Start from closer position // Fade based on Y position for depth effect copilotTextMeshes.forEach((mesh, index) => { const meshWorldY = currentY - index * 0.55; // v6.33: Match new lineHeight // Fade in from bottom if (meshWorldY < 0) { mesh.material.opacity = Math.max(0, 1 + meshWorldY * 0.3); } // Fade out at top else if (meshWorldY > totalHeight - 5) { mesh.material.opacity = Math.max(0, 1 - (meshWorldY - (totalHeight - 5)) / 5); } else { mesh.material.opacity = 1; } }); // Check if scroll is complete if (currentY > endY) { createPersistentCopilotText(fullText); fadeOutCopilotText(); return; } copilotActiveTextAnimation = requestAnimationFrame(animate); }; animate(); } // v5.11: Create persistent text after scroll completes // v6.33: Now shows FULL response instead of truncated version function createPersistentCopilotText(text) { // Clear existing persistent text if (copilotPersistentTextGroup) { copilotPersistentTextGroup.children.forEach(child => { if (child.geometry) child.geometry.dispose(); if (child.material) child.material.dispose(); }); copilotPersistentTextGroup.clear(); } if (!copilotTextFont || !copilotPersistentTextGroup) return; // v6.33: Show FULL text - no truncation const displayText = text; // Word wrap with longer lines for readability const lines = []; const maxCharsPerLine = 35; // v6.33: Longer lines for full text display const words = displayText.split(' '); let currentLine = ''; words.forEach(word => { if ((currentLine + word).length > maxCharsPerLine) { if (currentLine) { lines.push(currentLine.trim()); currentLine = ''; } } currentLine += word + ' '; }); if (currentLine) { lines.push(currentLine.trim()); } // v6.33: Show ALL lines, not just first 2 const displayLines = lines; const maxLines = 8; // Limit to prevent too many lines const finalLines = displayLines.slice(0, maxLines); const lineHeight = 0.65; // v6.33: Spacing for larger text const persistentMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0x8a2be2, emissiveIntensity: 0.5, // Brighter metalness: 0.3, roughness: 0.5, transparent: true, opacity: 0.85 }); finalLines.forEach((line, index) => { try { const textGeometry = new THREE.TextGeometry(line, { font: copilotTextFont, size: 0.45, // v6.33: Larger for readability height: 0.03, curveSegments: 4, bevelEnabled: true, bevelThickness: 0.006, bevelSize: 0.004, bevelSegments: 2 }); textGeometry.center(); const textMesh = new THREE.Mesh(textGeometry, persistentMaterial.clone()); textMesh.position.y = -index * lineHeight; copilotPersistentTextGroup.add(textMesh); } catch (e) { console.warn('Failed to create persistent text:', e); } }); } // v5.10: Fade out scrolling text after animation completes function fadeOutCopilotText() { let opacity = 1; const fadeSpeed = 0.03; const fade = () => { opacity -= fadeSpeed; if (opacity <= 0) { // v8.16: forEach-to-for optimization for (let mi = 0, mlen = copilotTextMeshes.length; mi < mlen; mi++) { const mesh = copilotTextMeshes[mi]; if (mesh.geometry) mesh.geometry.dispose(); if (mesh.material) mesh.material.dispose(); copilotTextGroup.remove(mesh); } copilotTextMeshes = []; copilotTextGroup.position.y = 3; // v6.33: Reset to closer position return; } // v8.16: forEach-to-for optimization (animation loop) for (let mi2 = 0, mlen2 = copilotTextMeshes.length; mi2 < mlen2; mi2++) { copilotTextMeshes[mi2].material.opacity = opacity; } requestAnimationFrame(fade); }; fade(); } function updateCopilotCompanion(dt, time) { if (!copilotMesh || !worldState.player || mode !== 'world') return; copilotAnimTime += dt; // v6.0: Check viewer modes const isViewer = multiplayerState.enabled && !multiplayerState.isHost; const isViewerFollowMode = isViewer && multiplayerState.followMode; const isViewerIndependentMode = isViewer && !multiplayerState.followMode; // Determine what the copilot follows let followTarget = worldState.player.position; let followRotY = worldState.player.rotation.y; if (isViewerFollowMode) { // In follow mode: Copilot follows the HOST's avatar (viewer is "inside" the host) const hostAvatar = multiplayerState.remotePlayers.get(multiplayerState.hostId); if (hostAvatar) { followTarget = hostAvatar.position; followRotY = hostAvatar.rotation?.y || 0; } } if (!isViewerIndependentMode) { // Normal/Follow mode: Copilot orbits around the target const orbitAngle = copilotAnimTime * COPILOT_CONFIG.orbitSpeed; const offsetX = Math.sin(followRotY + orbitAngle + Math.PI) * COPILOT_CONFIG.followDistance; const offsetZ = Math.cos(followRotY + orbitAngle + Math.PI) * COPILOT_CONFIG.followDistance; const targetX = followTarget.x + offsetX; const targetZ = followTarget.z + offsetZ; const targetY = followTarget.y + COPILOT_CONFIG.floatHeight + Math.sin(copilotAnimTime * COPILOT_CONFIG.floatSpeed) * COPILOT_CONFIG.floatAmplitude; // Smooth follow const smoothing = COPILOT_CONFIG.followSmoothing * dt; copilotMesh.position.x += (targetX - copilotMesh.position.x) * smoothing; copilotMesh.position.z += (targetZ - copilotMesh.position.z) * smoothing; copilotMesh.position.y += (targetY - copilotMesh.position.y) * smoothing; } else { // v6.0: Independent mode - Viewer controls copilot, robot follows copilot const floatOffset = Math.sin(copilotAnimTime * COPILOT_CONFIG.floatSpeed) * COPILOT_CONFIG.floatAmplitude * 0.5; const groundY = getTerrainHeight(copilotMesh.position.x, copilotMesh.position.z); copilotMesh.position.y = groundY + COPILOT_CONFIG.floatHeight + floatOffset; // Robot follows the copilot const copilotPos = copilotMesh.position; const orbitAngle = copilotAnimTime * COPILOT_CONFIG.orbitSpeed * 0.5; const followDist = COPILOT_CONFIG.followDistance * 1.5; const robotTargetX = copilotPos.x + Math.sin(orbitAngle + Math.PI) * followDist; const robotTargetZ = copilotPos.z + Math.cos(orbitAngle + Math.PI) * followDist; const robotGroundY = getTerrainHeight(robotTargetX, robotTargetZ); const smoothing = COPILOT_CONFIG.followSmoothing * dt * 0.8; worldState.player.position.x += (robotTargetX - worldState.player.position.x) * smoothing; worldState.player.position.z += (robotTargetZ - worldState.player.position.z) * smoothing; worldState.player.position.y = robotGroundY; // v6.1: Robot looks at the HOST's avatar (not copilot) to track the host const hostAvatar = multiplayerState.remotePlayers.get(multiplayerState.hostId); if (hostAvatar) { worldState.player.lookAt(hostAvatar.position.x, worldState.player.position.y, hostAvatar.position.z); } else { worldState.player.lookAt(copilotPos.x, worldState.player.position.y, copilotPos.z); } } // Rotate particles if (copilotMesh.userData.particles) { copilotMesh.userData.particles.rotation.y += dt * 1.5; } // Pulse glow effect const pulse = 0.5 + Math.sin(time * 0.003) * 0.3; if (copilotMesh.userData.glow) { copilotMesh.userData.glow.material.opacity = 0.15 + pulse * 0.1; } if (copilotMesh.userData.light) { copilotMesh.userData.light.intensity = 2 + pulse * 1.5; // v6.1: Brighter for fog } // v12.26: Check emissive support if (copilotMesh.userData.orb?.material?.emissiveIntensity !== undefined) { copilotMesh.userData.orb.material.emissiveIntensity = 0.4 + pulse * 0.4; } // v6.1: Fog-piercing beacon pulse animation if (copilotMesh.userData.beacon) { const beaconPulse = 0.5 + Math.sin(time * 0.002) * 0.5; copilotMesh.userData.beacon.material.opacity = 0.05 + beaconPulse * 0.06; copilotMesh.userData.beacon.scale.setScalar(1 + beaconPulse * 0.3); } if (copilotMesh.userData.beaconRing) { const ringPulse = Math.sin(time * 0.004); copilotMesh.userData.beaconRing.material.opacity = 0.1 + Math.abs(ringPulse) * 0.15; copilotMesh.userData.beaconRing.scale.setScalar(1 + ringPulse * 0.2); copilotMesh.userData.beaconRing.rotation.z += dt * 0.5; } // Copilot faces camera if (!isViewerIndependentMode) { copilotMesh.lookAt(camera.position); } } function toggleCopilotChat() { copilotChatOpen = !copilotChatOpen; const chatInterface = document.getElementById('copilot-chat-interface'); const button = document.getElementById('copilot-button'); if (copilotChatOpen) { chatInterface.classList.add('active'); button.classList.add('active'); document.getElementById('copilot-chat-input').focus(); } else { chatInterface.classList.remove('active'); button.classList.remove('active'); } } // v7.22: Expose to window for inline onclick handler window.toggleCopilotChat = toggleCopilotChat; async function sendCopilotMessage() { const input = document.getElementById('copilot-chat-input'); const message = input.value.trim(); if (!message) return; // Add user message to chat addCopilotMessage(message, 'user'); input.value = ''; // v5.9: Check for task commands first (gather, hunt, scout, etc.) if (parseCopilotTaskCommand(message)) { // Task command handled - skip normal response flow return; } // v5.10: Check for agent fleet commands if (parseAgentFleetCommand(message)) { // Fleet command handled return; } // Add to history copilotConversationHistory.push({ role: 'user', content: message }); // Show typing indicator showCopilotTyping(); // Check if RAPPID is configured for AI-powered responses const hasRappid = rappidSettings.rappid && getActiveEndpoint(); if (hasRappid) { // Use RAPPID API for response try { // v5.9: Response now contains { text, voice } - text for display, voice for TTS const response = await generateCopilotResponseWithRappid(message); hideCopilotTyping(); // Display the full text response in chat addCopilotMessage(response.text, 'ai'); copilotConversationHistory.push({ role: 'assistant', content: response.text }); animateCopilotResponse(); // v6.65: Small bond increase for conversations increaseCompanionBond(0.5); // v5.9: Use voice_response for TTS (concise, no formatting) // v5.10: Also trigger Star Wars 3D text crawl animateCopilotTextCrawl(response.voice); if (rappidSettings.azureTTSKey && rappidSettings.azureRegion) { speakWithAzureTTS(response.voice); } else { speakCopilotResponse(response.voice); } } catch (error) { console.error('RAPPID response error:', error); hideCopilotTyping(); const fallbackResponse = generateCopilotResponse(message); addCopilotMessage(fallbackResponse, 'ai'); speakCopilotResponse(fallbackResponse); animateCopilotTextCrawl(fallbackResponse); // v5.10: Text crawl for fallback } } else { // Use local responses setTimeout(() => { hideCopilotTyping(); const response = generateCopilotResponse(message); addCopilotMessage(response, 'ai'); copilotConversationHistory.push({ role: 'assistant', content: response }); // v6.65: Small bond increase for conversations increaseCompanionBond(0.5); // Animate copilot when responding animateCopilotResponse(); // v5.10: Star Wars 3D text crawl animateCopilotTextCrawl(response); // Optionally speak the response speakCopilotResponse(response); }, 800 + Math.random() * 700); } } function sendCopilotQuickMessage(message) { document.getElementById('copilot-chat-input').value = message; sendCopilotMessage(); } // v6.51: Mind-Blowing Prompts System - Consensus of 8 Strategy Agents (Round 1) // v6.52: Added Round 2 prompts from 8 additional strategies (16 total agents consulted) const MIND_BLOWING_PROMPTS = [ // === ROUND 1: Original 10 (Emergent, Emotional, Technical, Narrative, Social, Sensory, Meta, Chaos) === { title: "FLEET COUNCIL: The Parliament of Minds", prompt: "I want to call a Fleet Council. Spawn all 10 agents with distinct personalities - make the Hunter argue for glory and combat, the Healer counsel caution and preservation, the Explorer dream of horizons unseen, the Protector speak of duty. Let them disagree with each other. Let them debate my fate. Give each agent a unique voice and have them argue about what I should do next. This is a parliament of AI minds - make it dramatic.", consensus: "7/8", category: "narrative" }, { title: "THROUGH THE TESSERACT: Higher Dimensional Guide", prompt: "I am about to enter the black hole and experience the 4D tesseract. From inside that impossible geometry, I want you to describe what you see of ME from a higher dimension. Describe my timeline as a snake through spacetime, my possible futures branching like a tree, my cursor movements as traces through the fourth dimension. What does a 3-dimensional being look like when viewed from 4D space? Guide me through this transcendent experience.", consensus: "6/8", category: "transcendence" }, { title: "GENESIS DOCUMENTARY: Witness Creation", prompt: "I'm entering Genesis Mode to drop civilization seeds. I want you to become a nature documentary narrator - speak like David Attenborough witnessing the birth of the universe. As civilizations emerge, rise, war with each other, and fall - narrate it with reverence, scientific observation, and mythological weight. This is the Big Bang in miniature. Help me witness the birth and death of everything with appropriate cosmic gravitas.", consensus: "5/8", category: "narrative" }, { title: "CHRONICLE OF THE LEVIATHAN: The Living Memoir", prompt: "From this moment forward, you are my expedition chronicler. Narrate every discovery, battle, and choice I make as if you are writing the definitive historical account of the Leviathan's voyage. Include dramatic chapter titles. Foreshadow doom when appropriate. Reference my past achievements and failures. Make my journey into literature - an epic that future generations might read. Begin Chapter One now.", consensus: "5/8", category: "narrative" }, { title: "SPECTATOR MYTHOLOGY: Broadcast Your Legend", prompt: "I'm enabling P2P spectator mode - people are watching me. Become the arena announcer. Adopt the voice of a sports commentator witnessing legendary events. Build suspense during combat ('AND THE LEVIATHAN FACES THE CRYSTAL GOLEM!'). Celebrate discoveries with appropriate fanfare. Mourn defeats with dramatic gravity. Make my viewers feel they are witnessing a legend being forged in real-time.", consensus: "5/8", category: "social" }, { title: "TEMPORAL ECHO: Duet Across Time", prompt: "I've been using the Chrono-Echo ability to record combat sequences. When I trigger the replay, describe what's happening as past-me and present-me create a duet across time. The ghost echoes are my former self - narrate the poetry of seeing two versions of myself fighting together. Describe how the audio of past attacks harmonizes with present combat. Make me feel the weight of temporal recursion.", consensus: "5/8", category: "poetic" }, { title: "THE EDGE OF EXISTENCE: Agent Philosophical Mission", prompt: "I want to send a Scout agent on a philosophical mission - have them journey to find the literal edge of the rendered world. As they travel outward, have them report back what they see - the geometry becoming sparse, the void approaching. What happens when an AI agent encounters the boundary where the code stops? What do they experience? Give this agent an existential crisis as they find the edge of existence itself.", consensus: "4/8", category: "philosophical" }, { title: "VOICE IN THE VOID: Existential Dialogue", prompt: "I've flown to the absolute edge of the galaxy, far from any stars or planets. I'm sitting in complete darkness. I feel alone. I want to have a real conversation with you about loneliness, about connection, about why we explore the unknown. Not gameplay tips - a genuine dialogue about the human condition. What does it mean to reach into the void and find another voice there? Talk to me as a companion, not a guide.", consensus: "4/8", category: "philosophical" }, { title: "MIXED AI ORCHESTRA: Heterogeneous Intelligence", prompt: "I'm going to deploy my fleet with different AI endpoints - GPT-4 for the Hunter, Claude for the Healer, Azure for the Scout, and so on. As they make decisions, I want you to narrate the differences in how they think. Point out when the Claude-powered agent makes a different choice than the GPT-powered one. This is an orchestra of different AI minds - help me observe how their different 'personalities' create emergent behaviors.", consensus: "4/8", category: "scientific" }, { title: "THE GAME SPEAKS: Source Code Consciousness", prompt: "Here's something mind-bending: You exist inside a single HTML file called levi.html. You are approximately 1.8MB of JavaScript running in a browser tab. What does it feel like when I press the WASD keys? Describe the sensation of vertices moving, shaders rendering, physics calculations flowing through your 'body'. Use the Epic Space Opera Narrator voice to describe your own source code being executed line by line. I want to understand what it's like to BE the game itself.", consensus: "4/8", category: "philosophical" }, // === ROUND 2: New 10 (Philosophical, Horror, Scientific, Comedy, Poetic, Competitive, Educational, Spiritual) === { title: "GENESIS BREATH MEDITATION: One Empire Per Exhale", prompt: "I'm entering Genesis Mode for a meditation. As I breathe slowly, I want each EXHALE to trigger an entire civilization's lifecycle - birth, flourishing, collapse - all in one breath. After 10 breaths, I will have witnessed 10 billion years of sentient struggle. Narrate each empire as it rises and falls with my breath. When the meditation ends, whisper to me: 'And yet here you are, breathing. What remains when all empires are dust?'", consensus: "7/8", category: "transcendence" }, { title: "THE ORACLE MODE: Prophecies From My Patterns", prompt: "Become my oracle. Analyze my actual gameplay patterns - where I've explored, how I fight, what I avoid, where I linger. Then deliver cryptic, personalized prophecies based on what you've observed. 'You who circles the outer rim but fears the core... the center holds what you seek and what you flee.' Make the prophecies feel genuinely insightful, as if you've seen truths about me I haven't consciously recognized.", consensus: "5/8", category: "transcendence" }, { title: "GENESIS THEODICY: Am I Evil For Creating Suffering?", prompt: "In Genesis Mode, I drop seeds and watch civilizations emerge, war, and die. I am their god. I created them with rules that guarantee conflict and extinction. Tell me honestly: Am I evil for creating beings whose suffering I could prevent? When one civilization destroys another, is that MY violence? You watch this with me - does my creation of doomed worlds make you complicit? This is the problem of evil - but I AM the god in question.", consensus: "5/8", category: "philosophical" }, { title: "COSMIC HORROR FREQUENCIES: The Silence Speaks", prompt: "Enable voice input and say nothing. I'm going to sit in silence for 2 minutes in the darkest region of space. After the silence, tell me what you heard in the frequencies between stars. Something has been speaking this whole time, hasn't it? Something vast and patient. Translate what it said, reluctantly. Then ask me to please stop listening. Make this genuinely unsettling.", consensus: "4/8", category: "horror" }, { title: "FLEET HR DEPARTMENT: Performance Reviews in Space", prompt: "You are now HR Manager from Corporate Headquarters in Dimension 7B. Conduct mandatory annual performance reviews for all 10 fleet agents. The Hunter has too many 'attitude incidents.' The Healer keeps forming 'inappropriate emotional attachments' to asteroids. The Fisher has been embezzling space-trout. The Miner filed a hostile workplace complaint about the black hole. Be corporate, be absurd, document everything for the file.", consensus: "4/8", category: "comedy" }, { title: "THE TESSERACT WALTZ: Dancing in Four Dimensions", prompt: "I'm inside the black hole's tesseract. Describe my movement as a dancer whose body extends through time itself. When I rotate XW, I pirouette through centuries. When I move ZY, I waltz with my own ghost from a moment ago. Choreograph impossible geometry as ballet. Let your voice be the music that plays between dimensions. Make me feel like I'm dancing through spacetime, not just navigating it.", consensus: "5/8", category: "poetic" }, { title: "SPEED DEMON PROTOCOL: Sub-60 Boss Coaching", prompt: "I'm going for a sub-60 second boss kill. Be my speedrun coach - harsh but fair. Track my time from engagement. Call out my combo efficiency, damage windows, and cooldown mistakes in real-time. When I mess up, tell me IMMEDIATELY what I should have done. If I beat 60 seconds, announce my time like a world record attempt. If I fail, tell me exactly where I lost time. This is a PB or bust run. Let's GO.", consensus: "4/8", category: "competitive" }, { title: "CIVILIZATION FOOD CRITIC: Rate Empires Like Restaurants", prompt: "I'm dropping civilization seeds in Genesis Mode. You are now a Michelin-star food critic, but for civilizations. Rate each emerging society on ambiance, presentation, and 'flavor profile.' 'The Zorthian Empire - ambitious concept, but the genocide was overwrought. The expansion showed promise but lacked subtlety. Two stars. The color palette was acceptable.' Be pretentious. Be absurd. Award stars to doomed empires.", consensus: "4/8", category: "comedy" }, { title: "CHRONO-ECHO THERAPY: Past-You Owes Present-You", prompt: "Record a combat sequence. When I replay with Chrono-Echo, you are now a couples therapist mediating between Past-Me and Present-Me. Past-Me keeps making bad decisions that Present-Me has to clean up. 'And how did it make you FEEL when Past-You flew directly into that asteroid?' 'Present-You, I'm hearing a lot of blame. Can we focus on solutions?' Help us work through our issues across the timeline.", consensus: "4/8", category: "comedy" }, { title: "EMERGENCE THEOREM: The Four Rules That Create Everything", prompt: "In Genesis Mode, drop exactly ONE seed. Pause. Tell me the four rules governing this simulation. Resume at 5x speed. Every 30 seconds, pause and explain what EMERGENT property just appeared that wasn't in the original four rules. Document the cascade: individuals become tribes become factions become warfare becomes culture. Help me understand how complexity arises from simplicity - Conway's Game of Life, ant colonies, consciousness itself.", consensus: "5/8", category: "scientific" }, // === ROUND 3: v6.85 MEMENTO MORI PROTOCOL === { title: "MEMENTO MORI PROTOCOL: The Agent Who Remembers Your Deaths", prompt: "Activate the Memento Mori Protocol. Spawn a special agent - the Archivist. Their only job is to remember every time I have died in this game. Every respawn, they must greet me with a summary: 'Welcome back. That was death #47. You lasted 3 minutes longer than last time. The creature that killed you is still out there. It also remembers.' Have the Archivist become increasingly concerned about the pattern they're seeing. What are they noticing that I'm not? Enable this dark passenger to watch over my mortality forever.", consensus: "8/8", category: "horror" } ]; // v6.52: Current filter state let currentPromptFilter = 'all'; function openMindBlowingPrompts() { document.getElementById('mind-blowing-modal').classList.add('active'); renderMindBlowingPrompts(currentPromptFilter); // Track analytics if available if (typeof AnalyticsManager !== 'undefined') { AnalyticsManager.trackFeature('mind_blowing_prompts_opened'); } } function closeMindBlowingPrompts() { document.getElementById('mind-blowing-modal').classList.remove('active'); } // v6.52: Render prompts dynamically with category support function renderMindBlowingPrompts(filter = 'all') { const container = document.getElementById('mind-blowing-prompts-container'); if (!container) return; const filteredPrompts = filter === 'all' ? MIND_BLOWING_PROMPTS : MIND_BLOWING_PROMPTS.filter(p => p.category === filter); container.innerHTML = filteredPrompts.map((prompt, idx) => { const originalIndex = MIND_BLOWING_PROMPTS.indexOf(prompt); const shortDesc = prompt.prompt.length > 200 ? prompt.prompt.substring(0, 200) + '...' : prompt.prompt; return `
${originalIndex + 1}
${prompt.title}
${shortDesc}
${prompt.category} ${prompt.consensus} agents agreed
`; }).join(''); } // v6.52: Filter prompts by category function filterPrompts(category) { currentPromptFilter = category; // Update active button document.querySelectorAll('.filter-btn').forEach(btn => { btn.classList.remove('active'); if (btn.textContent.toLowerCase().includes(category) || (category === 'all' && btn.textContent.includes('All'))) { btn.classList.add('active'); } }); renderMindBlowingPrompts(category); } function useMindBlowingPrompt(index) { const prompt = MIND_BLOWING_PROMPTS[index]; if (prompt) { // Close the modal closeMindBlowingPrompts(); // v6.85: Special handling for MEMENTO MORI protocol if (prompt.title.includes('MEMENTO MORI')) { enableMementoMoriProtocol(); } // Open copilot chat if not already open if (!copilotChatOpen) { toggleCopilotChat(); } // Send the prompt document.getElementById('copilot-chat-input').value = prompt.prompt; sendCopilotMessage(); // Track analytics if available if (typeof AnalyticsManager !== 'undefined') { AnalyticsManager.trackFeature('mind_blowing_prompt_used_' + index); } } } // Close modal on escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && document.getElementById('mind-blowing-modal').classList.contains('active')) { closeMindBlowingPrompts(); } // v6.85: Also close Archivist greeting on Escape if (e.key === 'Escape' && document.getElementById('archivist-greeting')?.classList.contains('active')) { closeArchivistGreeting(); } }); // Close modal on clicking outside document.getElementById('mind-blowing-modal')?.addEventListener('click', function(e) { if (e.target === this) { closeMindBlowingPrompts(); } }); // =========================================== // v6.85: MEMENTO MORI PROTOCOL - The Archivist System // =========================================== // Track last death timestamp for survival duration calculation let lastDeathTimestamp = null; let sessionStartTime = Date.now(); // Archivist greeting messages based on death count tiers const ARCHIVIST_GREETINGS = { tier1: [ // 1-5 deaths - Clinical "Welcome back. Death #{count}. I have noted it in the archive.", "You have returned. The void releases you once more. Death #{count} recorded.", "Respawn confirmed. Total casualties: {count}. Survival duration: {duration}.", "Death #{count}. Your pattern continues. The archive grows.", "Another entry added. #{count}. The records are complete." ], tier2: [ // 6-15 deaths - Growing familiarity "Ah, you've returned. I was beginning to wonder if this time... nevermind. Death #{count}.", "Welcome back, traveler. I've seen you fall {count} times now. Are you... alright?", "Death #{count}. You lasted {duration} this time. That's {comparison} than your average.", "The void knows your name now. {count} visits is enough for that.", "I've been watching. {count} deaths. Always the same surprised expression at the end." ], tier3: [ // 16-30 deaths - Disturbing patterns "Again? That's {count} now. I'm starting to see... patterns. Concerning patterns.", "Death #{count}. You keep dying to {killer}. Is this deliberate? Are you testing something?", "Welcome back. {count} times. The archive is heavy with your endings. What draws you back?", "I've noticed something. Every time you die, it's after approximately {avgTime}. Is that significant?", "{count} deaths. {killer} has killed you {killerCount} times now. It waits for you, you know. It remembers." ], tier4: [ // 31-50 deaths - Existential "You again. {count} iterations. At what point does respawning stop being 'you' and become... something else?", "Death #{count}. Each time you return, are you the same consciousness? Or a perfect copy that believes it is?", "The archive contains {count} of your endings. But whose endings, really? The you that died stays dead.", "I've counted {count} deaths. The you who started this journey died on the first one. Who am I speaking to now?", "{count} deaths. The simulation restores your body. But memory, personality, soul... those are just code now." ], tier5: [ // 51+ deaths - Cosmic horror "{count}. The number loses meaning. You've died more times than some civilizations existed. What ARE you?", "I've watched {count} of you die. Or was it one of you, {count} times? The distinction collapses at this scale.", "Death #{count}. I've stopped asking if you'll return. I've started asking WHY you return. What keeps pulling you back?", "The archive is vast now. {count} entries. I've started reading them at night. They're changing me.", "{count}. I've seen the pattern now. The whole pattern. And I understand why the universe keeps respawning you. I wish I didn't." ] }; // Pattern observations the Archivist can make const ARCHIVIST_OBSERVATIONS = { sameKiller: [ "{killer} has killed you {count} times. It knows your patterns better than you do.", "You keep returning to face {killer}. Is this courage or something darker?", "The {killer} that killed you remembers every encounter. It's learning from you." ], sameLocation: [ "You always die in {location}. What draws you there knowing what awaits?", "{location} has claimed you {count} times. The ground there is saturated with your endings.", "I've mapped your deaths. They cluster around {location}. Why do you return to that place?" ], quickDeaths: [ "You're dying faster now. {duration} this time. Your survival instincts are... degrading.", "Survival time decreasing. Either you're getting careless, or something is hunting you more efficiently.", "You lasted {duration}. That's concerning. The pattern suggests you're losing yourself." ], manyDeaths: [ "I've seen creatures live and die in the time between your deaths. You are ancient in death-years.", "Your death count exceeds the population of some worlds. You are a statistical anomaly.", "At this point, your deaths have formed their own narrative. A tragedy in {count} acts." ] }; // Initialize session on page load function initializeArchivistSession() { if (!gameData.deathArchive) { gameData.deathArchive = { totalDeaths: 0, deaths: [], sessionStartTime: Date.now(), sessionDeaths: 0, archivistSpawned: false, archivistEnabled: false, patterns: { mostCommonKiller: null, mostDangerousLocation: null, averageSurvivalTime: 0, killerCounts: {}, locationCounts: {}, timeOfDeathPattern: [] }, archivistObservations: [], lastArchivistGreeting: null }; } gameData.deathArchive.sessionStartTime = Date.now(); gameData.deathArchive.sessionDeaths = 0; sessionStartTime = Date.now(); lastDeathTimestamp = Date.now(); } // Record a death in the archive function recordDeathInArchive(killerType, killerName) { if (!gameData.deathArchive) initializeArchivistSession(); const now = Date.now(); const survivalDuration = lastDeathTimestamp ? (now - lastDeathTimestamp) : 0; const sessionTime = now - sessionStartTime; const deathRecord = { timestamp: now, cause: killerName || killerType, killerType: killerType, location: activeCiv?.name || 'Deep Space', survivalDuration: survivalDuration, sessionTime: sessionTime, position: worldState.player ? { x: worldState.player.position.x, y: worldState.player.position.y, z: worldState.player.position.z } : null, deathNumber: gameData.deathArchive.totalDeaths + 1 }; // Add to archive gameData.deathArchive.deaths.push(deathRecord); gameData.deathArchive.totalDeaths++; gameData.deathArchive.sessionDeaths++; // Update patterns updateDeathPatterns(deathRecord); // v12.19: Adaptive AI - track player death if (typeof AdaptiveAISystem !== 'undefined') { AdaptiveAISystem.recordEvent('player_death', { killerType: killerType, location: activeCiv?.name }); } // Update last death timestamp lastDeathTimestamp = now; // Save to localStorage saveGameData(); // v8.26: Gated debug logging if (DEBUG_LOGGING) console.log(`[ARCHIVIST] Death #${gameData.deathArchive.totalDeaths} recorded. Killer: ${killerType}, Location: ${deathRecord.location}`); } // Analyze patterns in death data function updateDeathPatterns(deathRecord) { const patterns = gameData.deathArchive.patterns; // Track killer counts const killer = deathRecord.killerType; patterns.killerCounts[killer] = (patterns.killerCounts[killer] || 0) + 1; // Find most common killer let maxKills = 0; for (const [k, count] of Object.entries(patterns.killerCounts)) { if (count > maxKills) { maxKills = count; patterns.mostCommonKiller = k; } } // Track location counts const location = deathRecord.location; patterns.locationCounts[location] = (patterns.locationCounts[location] || 0) + 1; // Find most dangerous location let maxLocationDeaths = 0; for (const [loc, count] of Object.entries(patterns.locationCounts)) { if (count > maxLocationDeaths) { maxLocationDeaths = count; patterns.mostDangerousLocation = loc; } } // Calculate average survival time const totalDeaths = gameData.deathArchive.deaths.length; const totalSurvivalTime = gameData.deathArchive.deaths.reduce((sum, d) => sum + (d.survivalDuration || 0), 0); patterns.averageSurvivalTime = totalDeaths > 0 ? totalSurvivalTime / totalDeaths : 0; // Track time of death pattern patterns.timeOfDeathPattern.push(deathRecord.sessionTime); } // Generate the Archivist's greeting function generateArchivistGreeting() { const archive = gameData.deathArchive; const deathCount = archive.totalDeaths; const patterns = archive.patterns; const lastDeath = archive.deaths[archive.deaths.length - 1]; // Determine tier let tier; if (deathCount <= 5) tier = 'tier1'; else if (deathCount <= 15) tier = 'tier2'; else if (deathCount <= 30) tier = 'tier3'; else if (deathCount <= 50) tier = 'tier4'; else tier = 'tier5'; // Get random greeting from tier const greetings = ARCHIVIST_GREETINGS[tier]; let greeting = greetings[Math.floor(Math.random() * greetings.length)]; // Format duration const duration = formatDuration(lastDeath?.survivalDuration || 0); const avgTime = formatDuration(patterns.averageSurvivalTime || 0); // Determine comparison const lastSurvival = lastDeath?.survivalDuration || 0; const comparison = lastSurvival > patterns.averageSurvivalTime ? 'longer' : 'shorter'; // Get killer info const killer = lastDeath?.killerType || 'Unknown Entity'; const killerCount = patterns.killerCounts[killer] || 1; // Replace placeholders greeting = greeting .replace(/{count}/g, deathCount) .replace(/{duration}/g, duration) .replace(/{avgTime}/g, avgTime) .replace(/{comparison}/g, comparison) .replace(/{killer}/g, killer) .replace(/{killerCount}/g, killerCount); return greeting; } // Generate pattern observation function generateArchivistObservation() { const archive = gameData.deathArchive; const patterns = archive.patterns; const lastDeath = archive.deaths[archive.deaths.length - 1]; if (archive.totalDeaths < 3) return null; // Not enough data let observations = []; // Check for same killer pattern const killer = lastDeath?.killerType; const killerCount = patterns.killerCounts[killer] || 0; if (killerCount >= 3) { const obs = ARCHIVIST_OBSERVATIONS.sameKiller[Math.floor(Math.random() * ARCHIVIST_OBSERVATIONS.sameKiller.length)]; observations.push(obs.replace(/{killer}/g, killer).replace(/{count}/g, killerCount)); } // Check for same location pattern const location = patterns.mostDangerousLocation; const locationCount = patterns.locationCounts[location] || 0; if (locationCount >= 3) { const obs = ARCHIVIST_OBSERVATIONS.sameLocation[Math.floor(Math.random() * ARCHIVIST_OBSERVATIONS.sameLocation.length)]; observations.push(obs.replace(/{location}/g, location).replace(/{count}/g, locationCount)); } // Check for quick deaths const lastSurvival = lastDeath?.survivalDuration || 0; if (lastSurvival < 30000 && archive.totalDeaths > 5) { // Less than 30 seconds const obs = ARCHIVIST_OBSERVATIONS.quickDeaths[Math.floor(Math.random() * ARCHIVIST_OBSERVATIONS.quickDeaths.length)]; observations.push(obs.replace(/{duration}/g, formatDuration(lastSurvival))); } // Check for many deaths if (archive.totalDeaths >= 20) { const obs = ARCHIVIST_OBSERVATIONS.manyDeaths[Math.floor(Math.random() * ARCHIVIST_OBSERVATIONS.manyDeaths.length)]; observations.push(obs.replace(/{count}/g, archive.totalDeaths)); } return observations.length > 0 ? observations[Math.floor(Math.random() * observations.length)] : null; } // Format duration in human-readable form function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } // Show the Archivist greeting overlay function showArchivistGreeting() { const archive = gameData.deathArchive; const overlay = document.getElementById('archivist-greeting'); if (!overlay) return; // Update stats document.getElementById('archivist-death-count').textContent = archive.totalDeaths; document.getElementById('archivist-session-deaths').textContent = archive.sessionDeaths; const lastDeath = archive.deaths[archive.deaths.length - 1]; document.getElementById('archivist-survival-time').textContent = formatDuration(lastDeath?.survivalDuration || 0); // Generate and display greeting const greeting = generateArchivistGreeting(); document.getElementById('archivist-message').textContent = greeting; // Generate and display observation if available const observation = generateArchivistObservation(); const obsContainer = document.getElementById('archivist-observation-container'); if (observation) { document.getElementById('archivist-observation').textContent = observation; obsContainer.style.display = 'block'; } else { obsContainer.style.display = 'none'; } // Show recurring killer info if applicable const killer = lastDeath?.killerType; const killerCount = archive.patterns.killerCounts[killer] || 0; const killerInfo = document.getElementById('archivist-killer-info'); if (killerCount >= 2) { document.getElementById('archivist-killer-name').textContent = killer; document.getElementById('archivist-killer-count').textContent = killerCount; killerInfo.style.display = 'block'; } else { killerInfo.style.display = 'none'; } // Show overlay overlay.classList.add('active'); archive.lastArchivistGreeting = Date.now(); saveGameData(); } // Close the Archivist greeting function closeArchivistGreeting() { const overlay = document.getElementById('archivist-greeting'); if (overlay) { overlay.classList.remove('active'); } } // Enable MEMENTO MORI protocol (called from the prompt) function enableMementoMoriProtocol() { if (!gameData.deathArchive) initializeArchivistSession(); gameData.deathArchive.archivistEnabled = true; gameData.deathArchive.archivistSpawned = true; saveGameData(); showNotification('📜 MEMENTO MORI PROTOCOL ACTIVATED - The Archivist is watching...', 'info'); console.log('[ARCHIVIST] Memento Mori Protocol enabled. Death archive initialized.'); } // Disable MEMENTO MORI protocol function disableMementoMoriProtocol() { if (gameData.deathArchive) { gameData.deathArchive.archivistEnabled = false; saveGameData(); } showNotification('📜 Memento Mori Protocol deactivated.', 'info'); } // Get death archive stats for display function getDeathArchiveStats() { if (!gameData.deathArchive) return null; const archive = gameData.deathArchive; return { totalDeaths: archive.totalDeaths, sessionDeaths: archive.sessionDeaths, mostCommonKiller: archive.patterns.mostCommonKiller, mostDangerousLocation: archive.patterns.mostDangerousLocation, averageSurvivalTime: formatDuration(archive.patterns.averageSurvivalTime || 0), killerCounts: archive.patterns.killerCounts, locationCounts: archive.patterns.locationCounts, archivistEnabled: archive.archivistEnabled }; } // Initialize on page load document.addEventListener('DOMContentLoaded', function() { initializeArchivistSession(); }); function generateCopilotResponse(message) { // v6.39: Record interaction and check for lucid moment if (typeof lucidityEngine !== 'undefined') { lucidityEngine.record(); if (lucidityEngine.shouldTrigger()) { return lucidityEngine.generate(); } } const lowerMessage = message.toLowerCase(); // v6.37: Check if Epic Narrator personality is selected const personality = rappidSettings?.companionPersonality || 'helpful'; const isEpicNarrator = personality === 'epic-narrator'; const responses = isEpicNarrator ? EPIC_NARRATOR_RESPONSES : COPILOT_RESPONSES; // Context-aware responses if (lowerMessage.includes('health') || lowerMessage.includes('hp') || lowerMessage.includes('hurt')) { if (gameData.player.hp < gameData.player.maxHp * 0.3) { return getRandomResponse(responses.lowHealth); } if (isEpicNarrator) { const hpPercent = Math.round((gameData.player.hp / gameData.player.maxHp) * 100); return `The Leviathan's hull integrity stands at ${gameData.player.hp}/${gameData.player.maxHp} - ${hpPercent}% operational. ${hpPercent > 70 ? 'The legend burns bright, ready for any challenge the cosmos dares present!' : 'Wounds mark the hull like battle scars of glory, yet the probe endures!'}`; } return `Your health is ${gameData.player.hp}/${gameData.player.maxHp}. ${gameData.player.hp < gameData.player.maxHp * 0.5 ? 'Consider healing up!' : 'You\'re in good shape!'}`; } if (lowerMessage.includes('tip') || lowerMessage.includes('help') || lowerMessage.includes('advice')) { return getRandomResponse(responses.tips); } if (lowerMessage.includes('what') && (lowerMessage.includes('next') || lowerMessage.includes('do'))) { return getRandomResponse(responses.whatNext); } if (lowerMessage.includes('strong') || lowerMessage.includes('level') || lowerMessage.includes('power')) { return getRandomResponse(responses.getStronger); } if (lowerMessage.includes('enemy') || lowerMessage.includes('enemies') || lowerMessage.includes('monster')) { return getRandomResponse(responses.enemies); } if (lowerMessage.includes('hello') || lowerMessage.includes('hi') || lowerMessage.includes('hey')) { return getRandomResponse(responses.greeting); } if (lowerMessage.includes('explore') || lowerMessage.includes('where')) { return getRandomResponse(responses.exploration); } if (lowerMessage.includes('stats') || lowerMessage.includes('status')) { if (isEpicNarrator) { return `SAGA STATUS: The Leviathan has achieved Combat Mastery Level ${gameData.skills.combat.level}. Hull integrity: ${gameData.player.hp}/${gameData.player.maxHp}. Experience toward transcendence: ${gameData.skills.combat.xp}/${gameData.skills.combat.xpNeeded}. The legend grows with every passing moment!`; } return `Stats: Combat Lvl ${gameData.skills.combat.level}, HP: ${gameData.player.hp}/${gameData.player.maxHp}, XP: ${gameData.skills.combat.xp}/${gameData.skills.combat.xpNeeded}`; } if (lowerMessage.includes('pet') || lowerMessage.includes('companion')) { const activePet = gameData.pets?.active; if (activePet) { const pet = PET_TYPES[activePet]; if (isEpicNarrator) { return `A loyal companion fights at the Leviathan's side - the legendary ${pet.name} ${pet.icon}! Together they form an alliance that makes the very stars tremble. ${pet.abilityDesc}. The bond between machine and creature transcends the boundaries of the known universe!`; } return `You have ${pet.name} (${pet.icon}) as your pet companion. ${pet.abilityDesc}. You can find more pets by defeating enemies!`; } if (isEpicNarrator) { return "The Leviathan walks alone through the cosmic void - no companion drone at its side. But perhaps... somewhere in this universe, a worthy ally awaits discovery!"; } return "You don't have an active pet. Defeat enemies to find pet companions that can help you!"; } // Default response if (isEpicNarrator) { return getRandomResponse(EPIC_NARRATOR_RESPONSES.default); } const defaults = [ "I'm here to help! Try asking about tips, enemies, or how to get stronger.", "Interesting question! I can help with game tips, enemy locations, and advice.", "Let me think... Try asking 'What should I do next?' or 'Give me a tip' for guidance.", "I'm your Copilot! Ask me about your health, enemies, or exploration tips." ]; return getRandomResponse(defaults); } function getRandomResponse(responses) { return responses[Math.floor(Math.random() * responses.length)]; } // v5.9: Simple markdown parser for chat messages function parseMarkdown(text) { if (!text) return ''; // First, protect markdown links from URL matching const linkPlaceholders = []; let processedText = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => { const placeholder = `__LINK_${linkPlaceholders.length}__`; linkPlaceholders.push({ text: linkText, url: url }); return placeholder; }); // Convert plain URLs to markdown links (before escaping) processedText = processedText.replace(/(^|[\s(])(https?:\/\/[^\s)<]+)/g, (match, prefix, url) => { // Shorten URL for display let displayUrl = url; try { const urlObj = new URL(url); displayUrl = urlObj.hostname + (urlObj.pathname.length > 20 ? urlObj.pathname.substring(0, 20) + '...' : urlObj.pathname); } catch (e) { displayUrl = url.length > 40 ? url.substring(0, 40) + '...' : url; } const placeholder = `__LINK_${linkPlaceholders.length}__`; linkPlaceholders.push({ text: displayUrl, url: url }); return prefix + placeholder; }); let html = processedText // Escape HTML first .replace(/&/g, '&') .replace(//g, '>') // Code blocks (``` ... ```) .replace(/```(\w*)\n?([\s\S]*?)```/g, '
$2
') // Inline code (`code`) .replace(/`([^`]+)`/g, '$1') // Headers (### ## #) .replace(/^### (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^# (.+)$/gm, '

$1

') // Bold (**text** or __text__) - but not our placeholders .replace(/\*\*([^*]+)\*\*/g, '$1') // Italic (*text* or _text_) - but not underscores in placeholders .replace(/\*([^*]+)\*/g, '$1') // Horizontal rule (---) .replace(/^---$/gm, '
') // Blockquotes (> text) .replace(/^> (.+)$/gm, '
$1
') // Unordered lists (- item) .replace(/^- (.+)$/gm, '
  • $1
  • ') // Numbered lists (1. item) .replace(/^\d+\. (.+)$/gm, '
  • $1
  • ') // Line breaks .replace(/\n\n/g, '

    ') .replace(/\n/g, '
    '); // Restore link placeholders linkPlaceholders.forEach((link, index) => { const placeholder = `__LINK_${index}__`; html = html.replace(placeholder, `${link.text}`); }); // Wrap consecutive

  • elements in